mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-14 06:03:45 +01:00
Compare commits
63
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f65a48eb8 | ||
|
|
c1fb202139 | ||
|
|
8710772330 | ||
|
|
37652aaeef | ||
|
|
033e6ab5a5 | ||
|
|
f0a38e0868 | ||
|
|
e799444a9a | ||
|
|
cc66e0d990 | ||
|
|
2c953c8738 | ||
|
|
b721fbf481 | ||
|
|
f8cd7874ef | ||
|
|
56e0a12a8f | ||
|
|
95dd9eafa6 | ||
|
|
410eac15a2 | ||
|
|
25d7c1bd42 | ||
|
|
dff7c27496 | ||
|
|
d885217a6f | ||
|
|
0d665f4dda | ||
|
|
6f99004dde | ||
|
|
72d182f553 | ||
|
|
7b826ad5ac | ||
|
|
a5af5ce053 | ||
|
|
a313cf0bc7 | ||
|
|
b7c97898e4 | ||
|
|
2be928d820 | ||
|
|
03fa1be85b | ||
|
|
d18bdb907f | ||
|
|
927b07700c | ||
|
|
52c27d8e78 | ||
|
|
62f7c071bf | ||
|
|
b9d3f8e739 | ||
|
|
0ceee70efc | ||
|
|
fabbbc5d39 | ||
|
|
57586773b6 | ||
|
|
4c1b93d6ff | ||
|
|
4a289cc0a0 | ||
|
|
83c379e1e3 | ||
|
|
f2bf106633 | ||
|
|
9cc9997ba5 | ||
|
|
201e5a505b | ||
|
|
282c43ca47 | ||
|
|
c1b1c21a4d | ||
|
|
e50f802879 | ||
|
|
1c93b441fa | ||
|
|
19a657bac8 | ||
|
|
2487ff8c43 | ||
|
|
c274422eb6 | ||
|
|
54e19c4e87 | ||
|
|
f8444a4d50 | ||
|
|
f9489bdf1a | ||
|
|
232a4276ad | ||
|
|
b61083ee2a | ||
|
|
e3311bb8f3 | ||
|
|
b930b9c3e8 | ||
|
|
f734ab92a9 | ||
|
|
3962eed5ca | ||
|
|
13443e2bd4 | ||
|
|
519c1961ce | ||
|
|
730086b3f5 | ||
|
|
9c42a2ee54 | ||
|
|
dfd99d8ad0 | ||
|
|
905b21efb1 | ||
|
|
2a2d3ab36e |
@@ -21,7 +21,7 @@
|
||||
|
||||
[Overleaf](https://www.overleaf.com) is an open-source online real-time collaborative LaTeX editor. Overleaf runs a hosted version at [www.overleaf.com](https://www.overleaf.com), but you can also run your own local version, and contribute to the development of Overleaf.
|
||||
|
||||
## Extended Community Edition (CE+)
|
||||
## Extended Community Edition
|
||||
|
||||
The present "extended" version of Overleaf CE includes:
|
||||
|
||||
@@ -34,7 +34,6 @@ The present "extended" version of Overleaf CE includes:
|
||||
- Autocomplete of reference keys
|
||||
- Symbol Palette
|
||||
- "From External URL" feature
|
||||
- Advanced administrator tools for managing user accounts and projects
|
||||
|
||||
> [!CAUTION]
|
||||
> Overleaf Community Edition is intended for use in environments where **all** users are trusted. Community Edition is **not** appropriate for scenarios where isolation of users is required due to Sandbox Compiles not being available. When not using Sandboxed Compiles, users have full read and write access to the `sharelatex` container resources (filesystem, network, environment variables) when running LaTeX compiles.
|
||||
|
||||
@@ -129,7 +129,7 @@ services:
|
||||
dockerfile: services/real-time/Dockerfile
|
||||
env_file:
|
||||
- dev.env
|
||||
|
||||
|
||||
redis:
|
||||
image: redis:5
|
||||
ports:
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 525 KiB After Width: | Height: | Size: 1.0 MiB |
@@ -0,0 +1,7 @@
|
||||
# Autogenerated by build scripts. Do not edit.
|
||||
.eslint*
|
||||
.prettier*
|
||||
libraries/access-token-encryptor/**
|
||||
package-lock.json
|
||||
package.json
|
||||
patches/**
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
// Autogenerated by build scripts. Do not edit.
|
||||
|
||||
pipeline {
|
||||
agent {
|
||||
node {
|
||||
label 'jenkins-agent-web'
|
||||
customWorkspace '/workspace'
|
||||
}
|
||||
}
|
||||
options {
|
||||
timestamps()
|
||||
timeout(time: 15, unit: 'MINUTES')
|
||||
}
|
||||
environment {
|
||||
BRANCH_NAME = "${env.CHANGE_BRANCH ? env.CHANGE_BRANCH : env.BRANCH_NAME}"
|
||||
JENKINS_BUILD_NUMBER = "${BUILD_NUMBER}"
|
||||
BUILD_NUMBER = "${SHORT_SHA}_${BUILD_NUMBER}"
|
||||
COMMIT_SHA = "${GIT_COMMIT}"
|
||||
SHORT_SHA = "${GIT_COMMIT.take(7)}"
|
||||
}
|
||||
stages {
|
||||
stage('Stage 1') {
|
||||
parallel {
|
||||
stage('Install monorepo') {
|
||||
steps {
|
||||
retry(count: 3) {
|
||||
sh 'make monorepo_setup'
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Create reports folder') {
|
||||
steps {
|
||||
sh 'mkdir libraries/access-token-encryptor/reports'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Stage 2') {
|
||||
parallel {
|
||||
stage('Lint') {
|
||||
steps {
|
||||
sh 'bin/run -w /overleaf/libraries/access-token-encryptor monorepo npm run lint -- --format json --output-file reports/eslint.json'
|
||||
}
|
||||
post {
|
||||
always {
|
||||
sh """
|
||||
sed -i 's_"filePath":"/overleaf_"filePath":"/workspace_g' libraries/access-token-encryptor/reports/eslint.json
|
||||
"""
|
||||
recordIssues checksAnnotationScope: 'ALL', enabledForFailure: true, failOnError: true, id: 'access-token-encryptor-eslint', name: 'access-token-encryptor eslint', qualityGates: [[integerThreshold: 1, threshold: 1.0, type: 'TOTAL']], sourceCodeRetention: 'LAST_BUILD', tools: [esLint(pattern: 'libraries/access-token-encryptor/reports/eslint.json')]
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Format') {
|
||||
steps {
|
||||
sh 'bin/run -w /overleaf/libraries/access-token-encryptor monorepo npm run format'
|
||||
}
|
||||
}
|
||||
stage('Typecheck') {
|
||||
steps {
|
||||
sh 'bin/run -w /overleaf/libraries/access-token-encryptor monorepo npm run types:check'
|
||||
}
|
||||
}
|
||||
stage('Test') {
|
||||
steps {
|
||||
retry(count: 3) {
|
||||
sh 'bin/run -w /overleaf/libraries/access-token-encryptor monorepo npm run test:ci'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
post {
|
||||
always {
|
||||
junit checksName: 'access-token-encryptor test results', testResults: 'libraries/access-token-encryptor/reports/junit-*.xml'
|
||||
}
|
||||
cleanup {
|
||||
sh 'rm -rf libraries/access-token-encryptor/reports'
|
||||
sh 'make clean_jenkins -j10'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,5 +5,4 @@ access-token-encryptor
|
||||
--esmock-loader=False
|
||||
--is-library=True
|
||||
--node-version=22.18.0
|
||||
--pipeline-owner=🚉 Platform
|
||||
--public-repo=False
|
||||
|
||||
@@ -4,6 +4,5 @@ module.exports = {
|
||||
mochaFile: `reports/junit-mocha-${process.env.MOCHA_GREP}.xml`,
|
||||
includePending: true,
|
||||
jenkinsMode: true,
|
||||
output: true,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
module.exports = {
|
||||
rules: {
|
||||
'no-unnecessary-trans': require('./no-unnecessary-trans'),
|
||||
'prefer-kebab-url': require('./prefer-kebab-url'),
|
||||
'should-unescape-trans': require('./should-unescape-trans'),
|
||||
'no-generated-editor-themes': require('./no-generated-editor-themes'),
|
||||
'require-script-runner': require('./require-script-runner'),
|
||||
'require-vi-doMock-valid-path': require('./require-vi-doMock-valid-path'),
|
||||
'require-loading-label': require('./require-loading-label'),
|
||||
},
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'error',
|
||||
docs: {
|
||||
description:
|
||||
'Prohibit CodeMirror themes that are generated in a function',
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
':matches(ArrowFunctionExpression, FunctionDeclaration, FunctionExpression) CallExpression > MemberExpression[object.name="EditorView"]:matches([property.name="theme"],[property.name="baseTheme"])'(
|
||||
node
|
||||
) {
|
||||
context.report({
|
||||
node,
|
||||
message: `EditorView.theme and EditorView.baseTheme each add CSS to the page for every instance of the theme. Store the theme in a variable and reuse it instead.`,
|
||||
})
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
fixable: 'code',
|
||||
docs: {
|
||||
description: 'Prohibit Trans with no components or values',
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
'JSXOpeningElement[name.name="Trans"]'(node) {
|
||||
const attributes = new Map(
|
||||
node.attributes.map(attr => [attr.name.name, attr])
|
||||
)
|
||||
|
||||
if (!attributes.has('components')) {
|
||||
if (node.parent.children.length > 0) {
|
||||
context.report({
|
||||
node,
|
||||
message: `Trans components must not have child elements`,
|
||||
})
|
||||
} else if (attributes.has('values')) {
|
||||
context.report({
|
||||
node,
|
||||
message: `Use t('…') when there are no components`,
|
||||
})
|
||||
} else {
|
||||
context.report({
|
||||
node,
|
||||
message: `Use t('…') when there are no components`,
|
||||
fix(fixer) {
|
||||
const i18nKey = attributes.get('i18nKey').value.value
|
||||
|
||||
// Note: Prettier can fix indentation
|
||||
return fixer.replaceText(node.parent, `{t('${i18nKey}')}`)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"name": "@overleaf/eslint-plugin",
|
||||
"version": "0.1.0",
|
||||
"author": "Overleaf (https://www.overleaf.com)",
|
||||
"license": "AGPL-3.0-only",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"eslint": "^8.51.0",
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/parser": "^8.50.0"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "node rules.test.js"
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
// URL parts should be kebab-case, but we didn't have this rule in the past.
|
||||
// The ESLint rule `prefer-kebab-url` will ignore these "legacy" URL parts.
|
||||
|
||||
const ignoreWords = {
|
||||
snake: new Set([
|
||||
'clear_saml_data',
|
||||
'confirm_link',
|
||||
'confirm_university_domain',
|
||||
'create_recurly_account',
|
||||
'current_history_content',
|
||||
'current_user',
|
||||
'default_email',
|
||||
'disable_managed_users',
|
||||
'doc_snapshot',
|
||||
'enable_history_ranges_support',
|
||||
'features_override',
|
||||
'generate_password_reset_url',
|
||||
'get_assignment',
|
||||
'get_clone',
|
||||
'health_check',
|
||||
'institutional_emails',
|
||||
'latest_template',
|
||||
'link_after_saml_response',
|
||||
'linked_file',
|
||||
'metrics_segmentation',
|
||||
'new_users',
|
||||
'no_autostart_post_gateway',
|
||||
'personal_info',
|
||||
'planned_maintenance',
|
||||
'refresh_features',
|
||||
'register_admin',
|
||||
'register_ldap_admin',
|
||||
'register_saml_admin',
|
||||
'restore_file',
|
||||
'revert_file',
|
||||
'saved_vers',
|
||||
'send_test_email',
|
||||
'session_maintenance',
|
||||
'set_in_session',
|
||||
'sign_in_to_link',
|
||||
'split_test',
|
||||
'sso_configuration_test',
|
||||
'sso_email',
|
||||
'sso_enrollment',
|
||||
'track_changes',
|
||||
'update_admin',
|
||||
'user_details',
|
||||
]),
|
||||
camel: new Set([
|
||||
'addWorkflowScope',
|
||||
'aiErrorAssistant',
|
||||
'beginAuth',
|
||||
'brandVariationId',
|
||||
'closeEditor',
|
||||
'completeRegistration',
|
||||
'deactivateOldProjects',
|
||||
'deletedSubscription',
|
||||
'disconnectAllUsers',
|
||||
'editingSession',
|
||||
'emailSubscription',
|
||||
'enableManagedUsers',
|
||||
'externalCollaboration',
|
||||
'flushProjectToTpds',
|
||||
'indexAll',
|
||||
'offboardManagedUser',
|
||||
'openEditor',
|
||||
'perfTest',
|
||||
'pollDropboxForUser',
|
||||
'resendInvite',
|
||||
'resendManagedUserInvite',
|
||||
'salesContactForm',
|
||||
'showSupport',
|
||||
]),
|
||||
other: new Set([
|
||||
'Project',
|
||||
'disableSSO',
|
||||
'enableSSO',
|
||||
'resendSSOLinkInvite',
|
||||
'usersCSV',
|
||||
]),
|
||||
}
|
||||
|
||||
module.exports = { ignoreWords }
|
||||
@@ -1,91 +0,0 @@
|
||||
const _ = require('lodash')
|
||||
const { ignoreWords } = require('./prefer-kebab-url-ignore')
|
||||
|
||||
const removeTextBetweenBrackets = text => {
|
||||
while (text.includes('[') || text.includes('(')) {
|
||||
text = text.replaceAll(/\[[^[\]]*]/g, '')
|
||||
text = text.replaceAll(/\([^()]*\)/g, '')
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
const shouldIgnoreWord = str =>
|
||||
str.includes(':') ||
|
||||
str.includes('(') ||
|
||||
str === '*' ||
|
||||
str.match(/^[a-z0-9.]+$/) ||
|
||||
ignoreWords.snake.has(str) ||
|
||||
ignoreWords.camel.has(str) ||
|
||||
ignoreWords.other.has(str)
|
||||
|
||||
const getSuggestion = routePath => {
|
||||
if (typeof routePath === 'string') {
|
||||
const kebabed = routePath
|
||||
.split('/')
|
||||
.map(word => (shouldIgnoreWord(word) ? word : _.kebabCase(word)))
|
||||
.join('/')
|
||||
return kebabed === routePath ? null : `'${kebabed}'`
|
||||
}
|
||||
|
||||
if (routePath instanceof RegExp) {
|
||||
const words = removeTextBetweenBrackets(routePath.source).match(/[\w-]+/g)
|
||||
if (!words) return routePath
|
||||
|
||||
let newSource = routePath.source
|
||||
for (const word of words) {
|
||||
if (!shouldIgnoreWord(word)) {
|
||||
newSource = newSource.replaceAll(
|
||||
new RegExp(`\\b${word}\\b`, 'g'),
|
||||
_.kebabCase(word)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const kebabed = new RegExp(newSource, routePath.flags)
|
||||
return kebabed.source.toString() === routePath.source.toString()
|
||||
? null
|
||||
: kebabed
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
fixable: 'code',
|
||||
hasSuggestions: true,
|
||||
docs: {
|
||||
description: 'Enforce using kebab-case for URL paths',
|
||||
},
|
||||
},
|
||||
create: context => ({
|
||||
CallExpression(node) {
|
||||
if (
|
||||
node.callee.type === 'MemberExpression' &&
|
||||
node.arguments[0]?.type === 'Literal' &&
|
||||
[/app/i, /router/i].some(callee =>
|
||||
typeof callee === 'string'
|
||||
? node.callee.object.name === callee
|
||||
: callee.test(node.callee.object.name)
|
||||
) &&
|
||||
['get', 'post', 'put', 'delete'].includes(node.callee.property.name)
|
||||
) {
|
||||
const routePath = node.arguments[0].value
|
||||
|
||||
const suggestion = getSuggestion(routePath)
|
||||
|
||||
if (suggestion) {
|
||||
context.report({
|
||||
node: node.arguments[0],
|
||||
message: 'Route path should be in kebab-case.',
|
||||
suggest: [
|
||||
{
|
||||
desc: `Change to kebab-case: ${suggestion}`,
|
||||
fix: fixer => fixer.replaceText(node.arguments[0], suggestion),
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
}),
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
fixable: null,
|
||||
docs: {
|
||||
description:
|
||||
'Require loadingLabel prop when isLoading is specified on OLButton',
|
||||
},
|
||||
schema: [],
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
'JSXOpeningElement[name.name="OLButton"]'(node) {
|
||||
const attributes = new Map(
|
||||
node.attributes.map(attr => [attr.name?.name, attr])
|
||||
)
|
||||
|
||||
const isLoadingAttr = attributes.get('isLoading')
|
||||
const loadingLabelAttr = attributes.get('loadingLabel')
|
||||
|
||||
if (isLoadingAttr && !loadingLabelAttr) {
|
||||
const isLoadingValue = isLoadingAttr.value
|
||||
|
||||
if (
|
||||
!isLoadingValue ||
|
||||
(isLoadingValue.type === 'JSXExpressionContainer' &&
|
||||
isLoadingValue.expression.type === 'Literal' &&
|
||||
isLoadingValue.expression.value === true)
|
||||
) {
|
||||
context.report({
|
||||
node: isLoadingAttr,
|
||||
message:
|
||||
'Button with isLoading prop must also specify loadingLabel',
|
||||
})
|
||||
} else if (
|
||||
isLoadingValue.type === 'JSXExpressionContainer' &&
|
||||
isLoadingValue.expression.type !== 'Literal'
|
||||
) {
|
||||
context.report({
|
||||
node: isLoadingAttr,
|
||||
message:
|
||||
'Button with isLoading prop must also specify loadingLabel',
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Require Script Runner for scripts',
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
let hasImport = false
|
||||
|
||||
return {
|
||||
ImportDeclaration(node) {
|
||||
if (node.source.value.endsWith('lib/ScriptRunner.mjs')) {
|
||||
hasImport = true
|
||||
}
|
||||
},
|
||||
'Program:exit'() {
|
||||
if (!hasImport) {
|
||||
context.report({
|
||||
loc: { line: 1, column: 0 },
|
||||
message:
|
||||
'Please use Script Runner for scripts. Refer to the developer manual (https://manual.dev-overleaf.com/development/code/web_scripts/#monitor-script-execution-and-usage-with-script-runner) for more information.',
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
const path = require('node:path')
|
||||
const fs = require('node:fs')
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Ensure vi.doMock first argument is a resolvable path.',
|
||||
category: 'Best Practices',
|
||||
recommended: false,
|
||||
url: '',
|
||||
},
|
||||
fixable: 'code',
|
||||
hasSuggestions: true,
|
||||
schema: [],
|
||||
messages: {
|
||||
unresolvablePath:
|
||||
'The path "{{pathValue}}" in vi.doMock() cannot be resolved relative to the current file.',
|
||||
notAStringLiteral:
|
||||
'The first argument of vi.doMock() must be (or resolve to) a string literal representing a path.',
|
||||
noArguments: 'vi.doMock() called with no arguments.',
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const currentFilePath = context.getFilename()
|
||||
// ESLint can sometimes pass <text> or <input> for snippets not in a file
|
||||
if (currentFilePath === '<text>' || currentFilePath === '<input>') {
|
||||
return {}
|
||||
}
|
||||
const currentDirectory = path.dirname(currentFilePath)
|
||||
|
||||
function canResolve(modulePath) {
|
||||
try {
|
||||
require.resolve(path.resolve(currentDirectory, modulePath))
|
||||
return true
|
||||
} catch (e) {
|
||||
const absolutePath = path.resolve(currentDirectory, modulePath)
|
||||
const extensions = [
|
||||
'',
|
||||
'.js',
|
||||
'.mjs',
|
||||
'.ts',
|
||||
'.jsx',
|
||||
'.tsx',
|
||||
'.json',
|
||||
'.node',
|
||||
'/index.js',
|
||||
'/index.ts',
|
||||
] // Add common extensions
|
||||
for (const ext of extensions) {
|
||||
if (fs.existsSync(absolutePath + ext)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
CallExpression(node) {
|
||||
if (
|
||||
node.callee.type === 'MemberExpression' &&
|
||||
node.callee.object.type === 'Identifier' &&
|
||||
node.callee.object.name === 'vi' &&
|
||||
node.callee.property.type === 'Identifier' &&
|
||||
node.callee.property.name === 'doMock'
|
||||
) {
|
||||
if (node.arguments.length === 0) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'noArguments',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const firstArg = node.arguments[0]
|
||||
let pathValue = firstArg.value
|
||||
|
||||
if (
|
||||
firstArg.type !== 'Literal' ||
|
||||
typeof firstArg.value !== 'string'
|
||||
) {
|
||||
if (firstArg.type === 'Identifier') {
|
||||
const variable = context
|
||||
.getScope()
|
||||
.variables.find(v => v.name === firstArg.name)
|
||||
if (
|
||||
variable &&
|
||||
variable.defs.length > 0 &&
|
||||
variable.defs[0].node.init &&
|
||||
variable.defs[0].node.init.type === 'Literal' &&
|
||||
typeof variable.defs[0].node.init.value === 'string'
|
||||
) {
|
||||
pathValue = variable.defs[0].node.init.value
|
||||
if (canResolve(pathValue)) {
|
||||
return
|
||||
}
|
||||
// If the first argument was a variable that didn't resolve then we can't auto-fix it
|
||||
}
|
||||
}
|
||||
context.report({
|
||||
node: firstArg,
|
||||
messageId: 'notAStringLiteral',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!pathValue.startsWith('.')) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!canResolve(pathValue)) {
|
||||
const mjsPath = pathValue.replace('.js', '.mjs')
|
||||
const additionalReportOptions = {}
|
||||
if (canResolve(mjsPath)) {
|
||||
additionalReportOptions.fix = fixer =>
|
||||
fixer.replaceText(firstArg, `'${mjsPath}'`)
|
||||
additionalReportOptions.suggest = [
|
||||
{
|
||||
desc: `Replace with "${pathValue.replace('.js', '.mjs')}"`,
|
||||
fix: fixer => fixer.replaceText(firstArg, `'${mjsPath}'`),
|
||||
},
|
||||
]
|
||||
}
|
||||
context.report({
|
||||
node: firstArg,
|
||||
messageId: 'unresolvablePath',
|
||||
data: {
|
||||
pathValue,
|
||||
},
|
||||
...additionalReportOptions,
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
const { RuleTester } = require('eslint')
|
||||
const preferKebabUrl = require('./prefer-kebab-url')
|
||||
const noUnnecessaryTrans = require('./no-unnecessary-trans')
|
||||
const shouldUnescapeTrans = require('./should-unescape-trans')
|
||||
const noGeneratedEditorThemes = require('./no-generated-editor-themes')
|
||||
const viDoMockValidPath = require('./require-vi-doMock-valid-path')
|
||||
|
||||
const ruleTester = new RuleTester({
|
||||
parser: require.resolve('@typescript-eslint/parser'),
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
ecmaFeatures: { jsx: true },
|
||||
},
|
||||
})
|
||||
|
||||
ruleTester.run('prefer-kebab-url', preferKebabUrl, {
|
||||
valid: [
|
||||
{ code: `app.get('/foo-bar')` },
|
||||
{ code: `app.get('/foo-bar/:id')` },
|
||||
{ code: `router.post('/foo-bar')` },
|
||||
{ code: `router.get('/foo-bar/:id/:name/:age')` },
|
||||
{ code: `webRouter.get('/foo-bar/:user_id/(ProjectName)/get-info')` },
|
||||
{ code: `webApp.post('/foo-bar/:user_id/(ProjectName)/get-info')` },
|
||||
{
|
||||
code: `router.get(/^\\/download\\/project\\/([^/]*)\\/output\\/output\\.pdf$/)`,
|
||||
},
|
||||
{
|
||||
code: `webRouter.get(/^\\/project\\/([^/]*)\\/user\\/([0-9a-f]+)\\/build\\/([0-9a-f-]+)\\/output\\/(.*)$/)`,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: `app.get('/fooBar')`,
|
||||
errors: [{ message: 'Route path should be in kebab-case.' }],
|
||||
},
|
||||
{
|
||||
code: `app.get('/fooBar/:id')`,
|
||||
errors: [{ message: 'Route path should be in kebab-case.' }],
|
||||
},
|
||||
{
|
||||
code: `webRouter.get('/foo_bar/:id/FooBar/:name/fooBar')`,
|
||||
errors: [{ message: 'Route path should be in kebab-case.' }],
|
||||
},
|
||||
{
|
||||
code: `router.get(/^\\/downLoad\\/pro-ject\\/([^/]*)\\/OutPut\\/out-put\\.pdf$/)`,
|
||||
errors: [{ message: 'Route path should be in kebab-case.' }],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
ruleTester.run('no-unnecessary-trans', noUnnecessaryTrans, {
|
||||
valid: [
|
||||
{ code: `<Trans i18nKey="test" components={{ strong: <strong/> }}/>` },
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: `<Trans i18nKey="test" values={{ test: 'foo '}}/>`,
|
||||
errors: [{ message: `Use t('…') when there are no components` }],
|
||||
},
|
||||
{
|
||||
code: `<Trans i18nKey="test" />`,
|
||||
errors: [{ message: `Use t('…') when there are no components` }],
|
||||
output: `{t('test')}`,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
ruleTester.run('should-unescape-trans', shouldUnescapeTrans, {
|
||||
valid: [
|
||||
{
|
||||
code: `<Trans i18nKey="test" components={{ strong: <strong/> }}/>`,
|
||||
},
|
||||
{
|
||||
code: `<Trans i18nKey="test" values={{ foo: 'bar' }} components={{ strong: <strong/> }} shouldUnescape tOptions={{ interpolation: { escapeValue: true } }}/>`,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: `<Trans i18nKey="test" values={{ foo: 'bar' }} components={{ strong: <strong/> }} />`,
|
||||
errors: [{ message: 'Trans with values must have shouldUnescape' }],
|
||||
output: `<Trans i18nKey="test" values={{ foo: 'bar' }}\nshouldUnescape components={{ strong: <strong/> }} />`,
|
||||
},
|
||||
{
|
||||
code: `<Trans i18nKey="test" values={{ foo: 'bar' }} components={{ strong: <strong/> }} shouldUnescape />`,
|
||||
errors: [
|
||||
{
|
||||
message:
|
||||
'Trans with shouldUnescape must have tOptions.interpolation.escapeValue',
|
||||
},
|
||||
],
|
||||
output: `<Trans i18nKey="test" values={{ foo: 'bar' }} components={{ strong: <strong/> }} shouldUnescape\ntOptions={{ interpolation: { escapeValue: true } }} />`,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const noGeneratedEditorThemesError =
|
||||
'EditorView.theme and EditorView.baseTheme each add CSS to the page for every instance of the theme. Store the theme in a variable and reuse it instead.'
|
||||
ruleTester.run('no-generated-editor-themes', noGeneratedEditorThemes, {
|
||||
valid: [
|
||||
{
|
||||
code: `EditorView.theme({ '.cm-editor': { color: 'black' } })`,
|
||||
},
|
||||
{
|
||||
code: `const theme = EditorView.theme({ '.cm-editor': { color: 'black' } })`,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: `function createTheme() { return EditorView.theme({ '.cm-editor': { color: 'black' } }) }`,
|
||||
errors: [
|
||||
{
|
||||
message: noGeneratedEditorThemesError,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `() => EditorView.theme({ '.cm-editor': { color: 'black' } })`,
|
||||
errors: [
|
||||
{
|
||||
message: noGeneratedEditorThemesError,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `class Foo { createTheme() { return EditorView.theme({ '.cm-editor': { color: 'black' } }) } }`,
|
||||
errors: [
|
||||
{
|
||||
message: noGeneratedEditorThemesError,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
ruleTester.run('domock-require-valid-path', viDoMockValidPath, {
|
||||
valid: [
|
||||
{
|
||||
code: 'vi.doMock("./require-vi-doMock-valid-path.js")',
|
||||
filename: __filename,
|
||||
},
|
||||
{
|
||||
code: 'const filename = "./require-vi-doMock-valid-path.js"; vi.doMock(filename);',
|
||||
filename: __filename,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: "vi.doMock('./require-vi-doMock-valid-path2')",
|
||||
filename: __filename,
|
||||
errors: [
|
||||
{
|
||||
message:
|
||||
'The path "./require-vi-doMock-valid-path2" in vi.doMock() cannot be resolved relative to the current file.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'const filename = "./require-vi-doMock-valid-path2.js"; vi.doMock(filename);',
|
||||
filename: __filename,
|
||||
errors: [
|
||||
{
|
||||
message:
|
||||
'The first argument of vi.doMock() must be (or resolve to) a string literal representing a path.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
@@ -1,60 +0,0 @@
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
fixable: 'code',
|
||||
docs: {
|
||||
description: 'Ensure that Trans with values has shouldUnescape',
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
'JSXOpeningElement[name.name="Trans"]'(node) {
|
||||
const attributes = new Map(
|
||||
node.attributes.map(attr => [attr.name.name, attr])
|
||||
)
|
||||
|
||||
if (attributes.has('values') && !attributes.has('shouldUnescape')) {
|
||||
context.report({
|
||||
node,
|
||||
message: 'Trans with values must have shouldUnescape',
|
||||
fix(fixer) {
|
||||
return fixer.insertTextAfter(
|
||||
attributes.get('values'),
|
||||
'\nshouldUnescape' // Note: Prettier can fix indentation
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if (attributes.has('values') && attributes.has('shouldUnescape')) {
|
||||
const tOptions = attributes.get('tOptions')
|
||||
if (!tOptions) {
|
||||
context.report({
|
||||
node,
|
||||
message:
|
||||
'Trans with shouldUnescape must have tOptions.interpolation.escapeValue',
|
||||
fix(fixer) {
|
||||
return fixer.insertTextAfter(
|
||||
attributes.get('shouldUnescape'),
|
||||
'\ntOptions={{ interpolation: { escapeValue: true } }}' // Note: Prettier can fix indentation
|
||||
)
|
||||
},
|
||||
})
|
||||
} else {
|
||||
const property = tOptions.value.expression.properties
|
||||
.find(p => p.key.name === 'interpolation')
|
||||
?.value.properties.find(p => p.key.name === 'escapeValue')
|
||||
|
||||
if (property?.value.value !== true) {
|
||||
context.report({
|
||||
node,
|
||||
message:
|
||||
'Trans with shouldUnescape must have tOptions.interpolation.escapeValue set to true',
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.backend.json",
|
||||
"include": [
|
||||
"**/*.js",
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
# Autogenerated by build scripts. Do not edit.
|
||||
.eslint*
|
||||
.prettier*
|
||||
libraries/fetch-utils/**
|
||||
libraries/o-error/**
|
||||
package-lock.json
|
||||
package.json
|
||||
patches/**
|
||||
Vendored
+82
@@ -0,0 +1,82 @@
|
||||
// Autogenerated by build scripts. Do not edit.
|
||||
|
||||
pipeline {
|
||||
agent {
|
||||
node {
|
||||
label 'jenkins-agent-web'
|
||||
customWorkspace '/workspace'
|
||||
}
|
||||
}
|
||||
options {
|
||||
timestamps()
|
||||
timeout(time: 15, unit: 'MINUTES')
|
||||
}
|
||||
environment {
|
||||
BRANCH_NAME = "${env.CHANGE_BRANCH ? env.CHANGE_BRANCH : env.BRANCH_NAME}"
|
||||
JENKINS_BUILD_NUMBER = "${BUILD_NUMBER}"
|
||||
BUILD_NUMBER = "${SHORT_SHA}_${BUILD_NUMBER}"
|
||||
COMMIT_SHA = "${GIT_COMMIT}"
|
||||
SHORT_SHA = "${GIT_COMMIT.take(7)}"
|
||||
}
|
||||
stages {
|
||||
stage('Stage 1') {
|
||||
parallel {
|
||||
stage('Install monorepo') {
|
||||
steps {
|
||||
retry(count: 3) {
|
||||
sh 'make monorepo_setup'
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Create reports folder') {
|
||||
steps {
|
||||
sh 'mkdir libraries/fetch-utils/reports'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Stage 2') {
|
||||
parallel {
|
||||
stage('Lint') {
|
||||
steps {
|
||||
sh 'bin/run -w /overleaf/libraries/fetch-utils monorepo npm run lint -- --format json --output-file reports/eslint.json'
|
||||
}
|
||||
post {
|
||||
always {
|
||||
sh """
|
||||
sed -i 's_"filePath":"/overleaf_"filePath":"/workspace_g' libraries/fetch-utils/reports/eslint.json
|
||||
"""
|
||||
recordIssues checksAnnotationScope: 'ALL', enabledForFailure: true, failOnError: true, id: 'fetch-utils-eslint', name: 'fetch-utils eslint', qualityGates: [[integerThreshold: 1, threshold: 1.0, type: 'TOTAL']], sourceCodeRetention: 'LAST_BUILD', tools: [esLint(pattern: 'libraries/fetch-utils/reports/eslint.json')]
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Format') {
|
||||
steps {
|
||||
sh 'bin/run -w /overleaf/libraries/fetch-utils monorepo npm run format'
|
||||
}
|
||||
}
|
||||
stage('Typecheck') {
|
||||
steps {
|
||||
sh 'bin/run -w /overleaf/libraries/fetch-utils monorepo npm run types:check'
|
||||
}
|
||||
}
|
||||
stage('Test') {
|
||||
steps {
|
||||
retry(count: 3) {
|
||||
sh 'bin/run -w /overleaf/libraries/fetch-utils monorepo npm run test:ci'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
post {
|
||||
always {
|
||||
junit checksName: 'fetch-utils test results', testResults: 'libraries/fetch-utils/reports/junit-*.xml'
|
||||
}
|
||||
cleanup {
|
||||
sh 'rm -rf libraries/fetch-utils/reports'
|
||||
sh 'make clean_jenkins -j10'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,5 +5,4 @@ fetch-utils
|
||||
--esmock-loader=False
|
||||
--is-library=True
|
||||
--node-version=22.18.0
|
||||
--pipeline-owner=🚉 Platform
|
||||
--public-repo=False
|
||||
|
||||
@@ -17,10 +17,10 @@
|
||||
"license": "AGPL-3.0-only",
|
||||
"devDependencies": {
|
||||
"@types/node-fetch": "^2.6.13",
|
||||
"body-parser": "1.20.4",
|
||||
"body-parser": "^1.20.3",
|
||||
"chai": "^4.3.6",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"express": "4.22.1",
|
||||
"express": "^4.21.2",
|
||||
"mocha": "^11.1.0",
|
||||
"mocha-junit-reporter": "^2.2.1",
|
||||
"mocha-multi-reporters": "^1.5.1",
|
||||
|
||||
@@ -4,6 +4,5 @@ module.exports = {
|
||||
mochaFile: `reports/junit-mocha-${process.env.MOCHA_GREP}.xml`,
|
||||
includePending: true,
|
||||
jenkinsMode: true,
|
||||
output: true,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# Autogenerated by build scripts. Do not edit.
|
||||
.eslint*
|
||||
.prettier*
|
||||
libraries/fetch-utils/**
|
||||
libraries/logger/**
|
||||
libraries/o-error/**
|
||||
package-lock.json
|
||||
package.json
|
||||
patches/**
|
||||
Vendored
+82
@@ -0,0 +1,82 @@
|
||||
// Autogenerated by build scripts. Do not edit.
|
||||
|
||||
pipeline {
|
||||
agent {
|
||||
node {
|
||||
label 'jenkins-agent-web'
|
||||
customWorkspace '/workspace'
|
||||
}
|
||||
}
|
||||
options {
|
||||
timestamps()
|
||||
timeout(time: 15, unit: 'MINUTES')
|
||||
}
|
||||
environment {
|
||||
BRANCH_NAME = "${env.CHANGE_BRANCH ? env.CHANGE_BRANCH : env.BRANCH_NAME}"
|
||||
JENKINS_BUILD_NUMBER = "${BUILD_NUMBER}"
|
||||
BUILD_NUMBER = "${SHORT_SHA}_${BUILD_NUMBER}"
|
||||
COMMIT_SHA = "${GIT_COMMIT}"
|
||||
SHORT_SHA = "${GIT_COMMIT.take(7)}"
|
||||
}
|
||||
stages {
|
||||
stage('Stage 1') {
|
||||
parallel {
|
||||
stage('Install monorepo') {
|
||||
steps {
|
||||
retry(count: 3) {
|
||||
sh 'make monorepo_setup'
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Create reports folder') {
|
||||
steps {
|
||||
sh 'mkdir libraries/logger/reports'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Stage 2') {
|
||||
parallel {
|
||||
stage('Lint') {
|
||||
steps {
|
||||
sh 'bin/run -w /overleaf/libraries/logger monorepo npm run lint -- --format json --output-file reports/eslint.json'
|
||||
}
|
||||
post {
|
||||
always {
|
||||
sh """
|
||||
sed -i 's_"filePath":"/overleaf_"filePath":"/workspace_g' libraries/logger/reports/eslint.json
|
||||
"""
|
||||
recordIssues checksAnnotationScope: 'ALL', enabledForFailure: true, failOnError: true, id: 'logger-eslint', name: 'logger eslint', qualityGates: [[integerThreshold: 1, threshold: 1.0, type: 'TOTAL']], sourceCodeRetention: 'LAST_BUILD', tools: [esLint(pattern: 'libraries/logger/reports/eslint.json')]
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Format') {
|
||||
steps {
|
||||
sh 'bin/run -w /overleaf/libraries/logger monorepo npm run format'
|
||||
}
|
||||
}
|
||||
stage('Typecheck') {
|
||||
steps {
|
||||
sh 'bin/run -w /overleaf/libraries/logger monorepo npm run types:check'
|
||||
}
|
||||
}
|
||||
stage('Test') {
|
||||
steps {
|
||||
retry(count: 3) {
|
||||
sh 'bin/run -w /overleaf/libraries/logger monorepo npm run test:ci'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
post {
|
||||
always {
|
||||
junit checksName: 'logger test results', testResults: 'libraries/logger/reports/junit-*.xml'
|
||||
}
|
||||
cleanup {
|
||||
sh 'rm -rf libraries/logger/reports'
|
||||
sh 'make clean_jenkins -j10'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,5 +5,4 @@ logger
|
||||
--esmock-loader=False
|
||||
--is-library=True
|
||||
--node-version=22.18.0
|
||||
--pipeline-owner=🚉 Platform
|
||||
--public-repo=False
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
const logger = require('./')
|
||||
const bunyan = require('bunyan')
|
||||
const serializers = require('./serializers')
|
||||
|
||||
function testLogRecorder() {
|
||||
const currentTest = this.currentTest
|
||||
for (const level of ['error', 'fatal']) {
|
||||
logger[level] = (info, msg) => {
|
||||
const entry = { level, ...info, msg }
|
||||
for (const [name, fn] of Object.entries(serializers)) {
|
||||
if (name in entry) entry[name] = fn(entry[name])
|
||||
}
|
||||
currentTest.consoleErrors = (currentTest.consoleErrors || []).concat(
|
||||
JSON.stringify(info, bunyan.safeCycles())
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = testLogRecorder
|
||||
@@ -4,6 +4,5 @@ module.exports = {
|
||||
mochaFile: `reports/junit-mocha-${process.env.MOCHA_GREP}.xml`,
|
||||
includePending: true,
|
||||
jenkinsMode: true,
|
||||
output: true,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# Autogenerated by build scripts. Do not edit.
|
||||
.eslint*
|
||||
.prettier*
|
||||
libraries/metrics/**
|
||||
package-lock.json
|
||||
package.json
|
||||
patches/**
|
||||
Vendored
+82
@@ -0,0 +1,82 @@
|
||||
// Autogenerated by build scripts. Do not edit.
|
||||
|
||||
pipeline {
|
||||
agent {
|
||||
node {
|
||||
label 'jenkins-agent-web'
|
||||
customWorkspace '/workspace'
|
||||
}
|
||||
}
|
||||
options {
|
||||
timestamps()
|
||||
timeout(time: 15, unit: 'MINUTES')
|
||||
}
|
||||
environment {
|
||||
BRANCH_NAME = "${env.CHANGE_BRANCH ? env.CHANGE_BRANCH : env.BRANCH_NAME}"
|
||||
JENKINS_BUILD_NUMBER = "${BUILD_NUMBER}"
|
||||
BUILD_NUMBER = "${SHORT_SHA}_${BUILD_NUMBER}"
|
||||
COMMIT_SHA = "${GIT_COMMIT}"
|
||||
SHORT_SHA = "${GIT_COMMIT.take(7)}"
|
||||
}
|
||||
stages {
|
||||
stage('Stage 1') {
|
||||
parallel {
|
||||
stage('Install monorepo') {
|
||||
steps {
|
||||
retry(count: 3) {
|
||||
sh 'make monorepo_setup'
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Create reports folder') {
|
||||
steps {
|
||||
sh 'mkdir libraries/metrics/reports'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Stage 2') {
|
||||
parallel {
|
||||
stage('Lint') {
|
||||
steps {
|
||||
sh 'bin/run -w /overleaf/libraries/metrics monorepo npm run lint -- --format json --output-file reports/eslint.json'
|
||||
}
|
||||
post {
|
||||
always {
|
||||
sh """
|
||||
sed -i 's_"filePath":"/overleaf_"filePath":"/workspace_g' libraries/metrics/reports/eslint.json
|
||||
"""
|
||||
recordIssues checksAnnotationScope: 'ALL', enabledForFailure: true, failOnError: true, id: 'metrics-eslint', name: 'metrics eslint', qualityGates: [[integerThreshold: 1, threshold: 1.0, type: 'TOTAL']], sourceCodeRetention: 'LAST_BUILD', tools: [esLint(pattern: 'libraries/metrics/reports/eslint.json')]
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Format') {
|
||||
steps {
|
||||
sh 'bin/run -w /overleaf/libraries/metrics monorepo npm run format'
|
||||
}
|
||||
}
|
||||
stage('Typecheck') {
|
||||
steps {
|
||||
sh 'bin/run -w /overleaf/libraries/metrics monorepo npm run types:check'
|
||||
}
|
||||
}
|
||||
stage('Test') {
|
||||
steps {
|
||||
retry(count: 3) {
|
||||
sh 'bin/run -w /overleaf/libraries/metrics monorepo npm run test:ci'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
post {
|
||||
always {
|
||||
junit checksName: 'metrics test results', testResults: 'libraries/metrics/reports/junit-*.xml'
|
||||
}
|
||||
cleanup {
|
||||
sh 'rm -rf libraries/metrics/reports'
|
||||
sh 'make clean_jenkins -j10'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,5 +5,4 @@ metrics
|
||||
--esmock-loader=False
|
||||
--is-library=True
|
||||
--node-version=22.18.0
|
||||
--pipeline-owner=🚉 Platform
|
||||
--public-repo=False
|
||||
|
||||
@@ -97,12 +97,6 @@ function histogram(key, value, buckets, labels = {}) {
|
||||
}
|
||||
|
||||
class Timer {
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {number} sampleRate
|
||||
* @param {Record<string, any>} labels
|
||||
* @param {Array<number>} buckets
|
||||
*/
|
||||
constructor(key, sampleRate = 1, labels = {}, buckets = undefined) {
|
||||
if (typeof sampleRate === 'object') {
|
||||
// called with (key, labels, buckets)
|
||||
|
||||
@@ -4,6 +4,5 @@ module.exports = {
|
||||
mochaFile: `reports/junit-mocha-${process.env.MOCHA_GREP}.xml`,
|
||||
includePending: true,
|
||||
jenkinsMode: true,
|
||||
output: true,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# Autogenerated by build scripts. Do not edit.
|
||||
.eslint*
|
||||
.prettier*
|
||||
libraries/mongo-utils/**
|
||||
package-lock.json
|
||||
package.json
|
||||
patches/**
|
||||
Vendored
+72
@@ -0,0 +1,72 @@
|
||||
// Autogenerated by build scripts. Do not edit.
|
||||
|
||||
pipeline {
|
||||
agent {
|
||||
node {
|
||||
label 'jenkins-agent-web'
|
||||
customWorkspace '/workspace'
|
||||
}
|
||||
}
|
||||
options {
|
||||
timestamps()
|
||||
timeout(time: 15, unit: 'MINUTES')
|
||||
}
|
||||
environment {
|
||||
BRANCH_NAME = "${env.CHANGE_BRANCH ? env.CHANGE_BRANCH : env.BRANCH_NAME}"
|
||||
JENKINS_BUILD_NUMBER = "${BUILD_NUMBER}"
|
||||
BUILD_NUMBER = "${SHORT_SHA}_${BUILD_NUMBER}"
|
||||
COMMIT_SHA = "${GIT_COMMIT}"
|
||||
SHORT_SHA = "${GIT_COMMIT.take(7)}"
|
||||
}
|
||||
stages {
|
||||
stage('Stage 1') {
|
||||
parallel {
|
||||
stage('Install monorepo') {
|
||||
steps {
|
||||
retry(count: 3) {
|
||||
sh 'make monorepo_setup'
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Create reports folder') {
|
||||
steps {
|
||||
sh 'mkdir libraries/mongo-utils/reports'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Stage 2') {
|
||||
parallel {
|
||||
stage('Lint') {
|
||||
steps {
|
||||
sh 'bin/run -w /overleaf/libraries/mongo-utils monorepo npm run lint -- --format json --output-file reports/eslint.json'
|
||||
}
|
||||
post {
|
||||
always {
|
||||
sh """
|
||||
sed -i 's_"filePath":"/overleaf_"filePath":"/workspace_g' libraries/mongo-utils/reports/eslint.json
|
||||
"""
|
||||
recordIssues checksAnnotationScope: 'ALL', enabledForFailure: true, failOnError: true, id: 'mongo-utils-eslint', name: 'mongo-utils eslint', qualityGates: [[integerThreshold: 1, threshold: 1.0, type: 'TOTAL']], sourceCodeRetention: 'LAST_BUILD', tools: [esLint(pattern: 'libraries/mongo-utils/reports/eslint.json')]
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Format') {
|
||||
steps {
|
||||
sh 'bin/run -w /overleaf/libraries/mongo-utils monorepo npm run format'
|
||||
}
|
||||
}
|
||||
stage('Typecheck') {
|
||||
steps {
|
||||
sh 'bin/run -w /overleaf/libraries/mongo-utils monorepo npm run types:check'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
post {
|
||||
cleanup {
|
||||
sh 'rm -rf libraries/mongo-utils/reports'
|
||||
sh 'make clean_jenkins -j10'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,5 +5,4 @@ mongo-utils
|
||||
--esmock-loader=False
|
||||
--is-library=True
|
||||
--node-version=22.18.0
|
||||
--pipeline-owner=🚉 Platform
|
||||
--public-repo=False
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# Autogenerated by build scripts. Do not edit.
|
||||
.eslint*
|
||||
.prettier*
|
||||
libraries/o-error/**
|
||||
package-lock.json
|
||||
package.json
|
||||
patches/**
|
||||
Vendored
+82
@@ -0,0 +1,82 @@
|
||||
// Autogenerated by build scripts. Do not edit.
|
||||
|
||||
pipeline {
|
||||
agent {
|
||||
node {
|
||||
label 'jenkins-agent-web'
|
||||
customWorkspace '/workspace'
|
||||
}
|
||||
}
|
||||
options {
|
||||
timestamps()
|
||||
timeout(time: 15, unit: 'MINUTES')
|
||||
}
|
||||
environment {
|
||||
BRANCH_NAME = "${env.CHANGE_BRANCH ? env.CHANGE_BRANCH : env.BRANCH_NAME}"
|
||||
JENKINS_BUILD_NUMBER = "${BUILD_NUMBER}"
|
||||
BUILD_NUMBER = "${SHORT_SHA}_${BUILD_NUMBER}"
|
||||
COMMIT_SHA = "${GIT_COMMIT}"
|
||||
SHORT_SHA = "${GIT_COMMIT.take(7)}"
|
||||
}
|
||||
stages {
|
||||
stage('Stage 1') {
|
||||
parallel {
|
||||
stage('Install monorepo') {
|
||||
steps {
|
||||
retry(count: 3) {
|
||||
sh 'make monorepo_setup'
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Create reports folder') {
|
||||
steps {
|
||||
sh 'mkdir libraries/o-error/reports'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Stage 2') {
|
||||
parallel {
|
||||
stage('Lint') {
|
||||
steps {
|
||||
sh 'bin/run -w /overleaf/libraries/o-error monorepo npm run lint -- --format json --output-file reports/eslint.json'
|
||||
}
|
||||
post {
|
||||
always {
|
||||
sh """
|
||||
sed -i 's_"filePath":"/overleaf_"filePath":"/workspace_g' libraries/o-error/reports/eslint.json
|
||||
"""
|
||||
recordIssues checksAnnotationScope: 'ALL', enabledForFailure: true, failOnError: true, id: 'o-error-eslint', name: 'o-error eslint', qualityGates: [[integerThreshold: 1, threshold: 1.0, type: 'TOTAL']], sourceCodeRetention: 'LAST_BUILD', tools: [esLint(pattern: 'libraries/o-error/reports/eslint.json')]
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Format') {
|
||||
steps {
|
||||
sh 'bin/run -w /overleaf/libraries/o-error monorepo npm run format'
|
||||
}
|
||||
}
|
||||
stage('Typecheck') {
|
||||
steps {
|
||||
sh 'bin/run -w /overleaf/libraries/o-error monorepo npm run types:check'
|
||||
}
|
||||
}
|
||||
stage('Test') {
|
||||
steps {
|
||||
retry(count: 3) {
|
||||
sh 'bin/run -w /overleaf/libraries/o-error monorepo npm run test:ci'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
post {
|
||||
always {
|
||||
junit checksName: 'o-error test results', testResults: 'libraries/o-error/reports/junit-*.xml'
|
||||
}
|
||||
cleanup {
|
||||
sh 'rm -rf libraries/o-error/reports'
|
||||
sh 'make clean_jenkins -j10'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,5 +5,4 @@ o-error
|
||||
--esmock-loader=False
|
||||
--is-library=True
|
||||
--node-version=22.18.0
|
||||
--pipeline-owner=🚉 Platform
|
||||
--public-repo=False
|
||||
|
||||
@@ -4,6 +4,5 @@ module.exports = {
|
||||
mochaFile: `reports/junit-mocha-${process.env.MOCHA_GREP}.xml`,
|
||||
includePending: true,
|
||||
jenkinsMode: true,
|
||||
output: true,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# Autogenerated by build scripts. Do not edit.
|
||||
.eslint*
|
||||
.prettier*
|
||||
libraries/fetch-utils/**
|
||||
libraries/logger/**
|
||||
libraries/metrics/**
|
||||
libraries/o-error/**
|
||||
libraries/object-persistor/**
|
||||
libraries/stream-utils/**
|
||||
package-lock.json
|
||||
package.json
|
||||
patches/**
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
// Autogenerated by build scripts. Do not edit.
|
||||
|
||||
pipeline {
|
||||
agent {
|
||||
node {
|
||||
label 'jenkins-agent-web'
|
||||
customWorkspace '/workspace'
|
||||
}
|
||||
}
|
||||
options {
|
||||
timestamps()
|
||||
timeout(time: 15, unit: 'MINUTES')
|
||||
}
|
||||
environment {
|
||||
BRANCH_NAME = "${env.CHANGE_BRANCH ? env.CHANGE_BRANCH : env.BRANCH_NAME}"
|
||||
JENKINS_BUILD_NUMBER = "${BUILD_NUMBER}"
|
||||
BUILD_NUMBER = "${SHORT_SHA}_${BUILD_NUMBER}"
|
||||
COMMIT_SHA = "${GIT_COMMIT}"
|
||||
SHORT_SHA = "${GIT_COMMIT.take(7)}"
|
||||
}
|
||||
stages {
|
||||
stage('Stage 1') {
|
||||
parallel {
|
||||
stage('Install monorepo') {
|
||||
steps {
|
||||
retry(count: 3) {
|
||||
sh 'make monorepo_setup'
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Create reports folder') {
|
||||
steps {
|
||||
sh 'mkdir libraries/object-persistor/reports'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Stage 2') {
|
||||
parallel {
|
||||
stage('Lint') {
|
||||
steps {
|
||||
sh 'bin/run -w /overleaf/libraries/object-persistor monorepo npm run lint -- --format json --output-file reports/eslint.json'
|
||||
}
|
||||
post {
|
||||
always {
|
||||
sh """
|
||||
sed -i 's_"filePath":"/overleaf_"filePath":"/workspace_g' libraries/object-persistor/reports/eslint.json
|
||||
"""
|
||||
recordIssues checksAnnotationScope: 'ALL', enabledForFailure: true, failOnError: true, id: 'object-persistor-eslint', name: 'object-persistor eslint', qualityGates: [[integerThreshold: 1, threshold: 1.0, type: 'TOTAL']], sourceCodeRetention: 'LAST_BUILD', tools: [esLint(pattern: 'libraries/object-persistor/reports/eslint.json')]
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Format') {
|
||||
steps {
|
||||
sh 'bin/run -w /overleaf/libraries/object-persistor monorepo npm run format'
|
||||
}
|
||||
}
|
||||
stage('Typecheck') {
|
||||
steps {
|
||||
sh 'bin/run -w /overleaf/libraries/object-persistor monorepo npm run types:check'
|
||||
}
|
||||
}
|
||||
stage('Test') {
|
||||
steps {
|
||||
retry(count: 3) {
|
||||
sh 'bin/run -w /overleaf/libraries/object-persistor monorepo npm run test:ci'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
post {
|
||||
always {
|
||||
junit checksName: 'object-persistor test results', testResults: 'libraries/object-persistor/reports/junit-*.xml'
|
||||
}
|
||||
cleanup {
|
||||
sh 'rm -rf libraries/object-persistor/reports'
|
||||
sh 'make clean_jenkins -j10'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,5 +5,4 @@ object-persistor
|
||||
--esmock-loader=False
|
||||
--is-library=True
|
||||
--node-version=22.18.0
|
||||
--pipeline-owner=🚉 Platform
|
||||
--public-repo=False
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"@overleaf/o-error": "*",
|
||||
"@overleaf/stream-utils": "*",
|
||||
"fast-crc32c": "overleaf/node-fast-crc32c#aae6b2a4c7a7a159395df9cc6c38dfde702d6f51",
|
||||
"glob": "^12.0.0",
|
||||
"glob": "^7.1.6",
|
||||
"range-parser": "^1.2.1",
|
||||
"tiny-async-pool": "^1.1.0"
|
||||
},
|
||||
|
||||
@@ -160,38 +160,4 @@ module.exports = class AbstractPersistor {
|
||||
name,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* List objects in a directory, returning the full keys.
|
||||
*
|
||||
* Suitable only for directories where the number of keys is known to be small.
|
||||
*
|
||||
* @param {string} location
|
||||
* @param {string} prefix
|
||||
* @returns {Promise<Array<string>>}
|
||||
*/
|
||||
async listDirectoryKeys(location, prefix) {
|
||||
throw new NotImplementedError('method not implemented in persistor', {
|
||||
method: 'listDirectoryKeys',
|
||||
location,
|
||||
prefix,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* List objects in a directory, returning key and size information.
|
||||
*
|
||||
* Suitable only for directories where the number of keys is known to be small.
|
||||
*
|
||||
* @param {string} location
|
||||
* @param {string} prefix
|
||||
* @returns {Promise<Array<{key: string, size: number}>>}
|
||||
*/
|
||||
async listDirectoryStats(location, prefix) {
|
||||
throw new NotImplementedError('method not implemented in persistor', {
|
||||
method: 'listDirectoryStats',
|
||||
location,
|
||||
prefix,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
const crypto = require('node:crypto')
|
||||
const fs = require('node:fs')
|
||||
const fsPromises = require('node:fs/promises')
|
||||
const { glob } = require('glob')
|
||||
const globCallbacks = require('glob')
|
||||
const Path = require('node:path')
|
||||
const { PassThrough } = require('node:stream')
|
||||
const { pipeline } = require('node:stream/promises')
|
||||
const { promisify } = require('node:util')
|
||||
|
||||
const AbstractPersistor = require('./AbstractPersistor')
|
||||
const { ReadError, WriteError, NotImplementedError } = require('./Errors')
|
||||
const PersistorHelper = require('./PersistorHelper')
|
||||
|
||||
const glob = promisify(globCallbacks)
|
||||
|
||||
module.exports = class FSPersistor extends AbstractPersistor {
|
||||
constructor(settings = {}) {
|
||||
if (settings.storageClass) {
|
||||
@@ -194,50 +197,6 @@ module.exports = class FSPersistor extends AbstractPersistor {
|
||||
}
|
||||
}
|
||||
|
||||
async listDirectoryKeys(location, name) {
|
||||
const fsPath = this._getFsPath(location, name)
|
||||
const paths = await this._listDirectory(fsPath)
|
||||
|
||||
// Filter to only return files, not directories
|
||||
const files = []
|
||||
for (const path of paths) {
|
||||
try {
|
||||
const stat = await fsPromises.stat(path)
|
||||
if (stat.isFile()) {
|
||||
files.push(path)
|
||||
}
|
||||
} catch (err) {
|
||||
// ignore files that may have just been deleted
|
||||
if (err.code !== 'ENOENT') {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
async listDirectoryStats(location, name) {
|
||||
const fsPath = this._getFsPath(location, name)
|
||||
const paths = await this._listDirectory(fsPath)
|
||||
|
||||
// Filter to only return files, not directories, with their sizes
|
||||
const stats = []
|
||||
for (const path of paths) {
|
||||
try {
|
||||
const stat = await fsPromises.stat(path)
|
||||
if (stat.isFile()) {
|
||||
stats.push({ key: path, size: stat.size })
|
||||
}
|
||||
} catch (err) {
|
||||
// ignore files that may have just been deleted
|
||||
if (err.code !== 'ENOENT') {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
return stats
|
||||
}
|
||||
|
||||
async checkIfObjectExists(location, name) {
|
||||
const fsPath = this._getFsPath(location, name)
|
||||
try {
|
||||
@@ -346,8 +305,10 @@ module.exports = class FSPersistor extends AbstractPersistor {
|
||||
|
||||
async _listDirectory(path) {
|
||||
if (this.useSubdirectories) {
|
||||
// eslint-disable-next-line @typescript-eslint/return-await
|
||||
return await glob(Path.join(path, '**'))
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/return-await
|
||||
return await glob(`${path}_*`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,25 +288,23 @@ module.exports = class GcsPersistor extends AbstractPersistor {
|
||||
} while (query)
|
||||
}
|
||||
|
||||
async #listDirectory(bucketName, prefix) {
|
||||
async directorySize(bucketName, key) {
|
||||
let files
|
||||
const prefix = ensurePrefixIsDirectory(key)
|
||||
|
||||
try {
|
||||
const [response] = await this.storage
|
||||
.bucket(bucketName)
|
||||
.getFiles({ prefix })
|
||||
return response
|
||||
files = response
|
||||
} catch (err) {
|
||||
throw PersistorHelper.wrapError(
|
||||
err,
|
||||
'failed to list objects in GCS',
|
||||
{ bucketName, prefix },
|
||||
{ bucketName, key },
|
||||
ReadError
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async directorySize(bucketName, key) {
|
||||
const prefix = ensurePrefixIsDirectory(key)
|
||||
const files = await this.#listDirectory(bucketName, prefix)
|
||||
|
||||
return files.reduce(
|
||||
(acc, file) => parseInt(file.metadata.size, 10) + acc,
|
||||
@@ -314,25 +312,6 @@ module.exports = class GcsPersistor extends AbstractPersistor {
|
||||
)
|
||||
}
|
||||
|
||||
async listDirectoryKeys(bucketName, prefix) {
|
||||
const files = await this.#listDirectory(
|
||||
bucketName,
|
||||
ensurePrefixIsDirectory(prefix)
|
||||
)
|
||||
return files.map(file => file.name)
|
||||
}
|
||||
|
||||
async listDirectoryStats(bucketName, prefix) {
|
||||
const files = await this.#listDirectory(
|
||||
bucketName,
|
||||
ensurePrefixIsDirectory(prefix)
|
||||
)
|
||||
return files.map(file => ({
|
||||
key: file.name,
|
||||
size: parseInt(file.metadata.size, 10),
|
||||
}))
|
||||
}
|
||||
|
||||
async checkIfObjectExists(bucketName, key) {
|
||||
try {
|
||||
const [response] = await this.storage
|
||||
|
||||
@@ -417,20 +417,10 @@ class CachedPerProjectEncryptedS3Persistor {
|
||||
*
|
||||
* @param {string} bucketName
|
||||
* @param {string} path
|
||||
* @return {Promise<string[]>}
|
||||
* @return {Promise<ListDirectoryResult>}
|
||||
*/
|
||||
async listDirectoryKeys(bucketName, path) {
|
||||
return await this.#parent.listDirectoryKeys(bucketName, path)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} bucketName
|
||||
* @param {string} path
|
||||
* @return {Promise<Array<{key: string, size: number}>>}
|
||||
*/
|
||||
async listDirectoryStats(bucketName, path) {
|
||||
return await this.#parent.listDirectoryStats(bucketName, path)
|
||||
async listDirectory(bucketName, path) {
|
||||
return await this.#parent.listDirectory(bucketName, path)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -266,7 +266,7 @@ class S3Persistor extends AbstractPersistor {
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async deleteDirectory(bucketName, key, continuationToken) {
|
||||
const { contents, response } = await this.#listDirectory(
|
||||
const { contents, response } = await this.listDirectory(
|
||||
bucketName,
|
||||
key,
|
||||
continuationToken
|
||||
@@ -309,7 +309,7 @@ class S3Persistor extends AbstractPersistor {
|
||||
* @param {string} [continuationToken]
|
||||
* @return {Promise<ListDirectoryResult>}
|
||||
*/
|
||||
async #listDirectory(bucketName, key, continuationToken) {
|
||||
async listDirectory(bucketName, key, continuationToken) {
|
||||
let response
|
||||
const options = { Bucket: bucketName, Prefix: key }
|
||||
if (continuationToken) {
|
||||
@@ -535,59 +535,6 @@ class S3Persistor extends AbstractPersistor {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} bucketName
|
||||
* @param {string} prefix
|
||||
* @return {Promise<Array<string>>}
|
||||
*/
|
||||
async listDirectoryKeys(bucketName, prefix) {
|
||||
const keys = []
|
||||
let continuationToken
|
||||
|
||||
do {
|
||||
const { contents, response } = await this.#listDirectory(
|
||||
bucketName,
|
||||
prefix,
|
||||
continuationToken
|
||||
)
|
||||
keys.push(...contents.map(item => item.Key || ''))
|
||||
continuationToken = response.IsTruncated
|
||||
? response.NextContinuationToken
|
||||
: undefined
|
||||
} while (continuationToken)
|
||||
|
||||
return keys
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} bucketName
|
||||
* @param {string} prefix
|
||||
* @return {Promise<Array<{key: string, size: number}>>}
|
||||
*/
|
||||
async listDirectoryStats(bucketName, prefix) {
|
||||
const stats = []
|
||||
let continuationToken
|
||||
|
||||
do {
|
||||
const { contents, response } = await this.#listDirectory(
|
||||
bucketName,
|
||||
prefix,
|
||||
continuationToken
|
||||
)
|
||||
stats.push(
|
||||
...contents.map(item => ({
|
||||
key: item.Key || '',
|
||||
size: item.Size ?? -1,
|
||||
}))
|
||||
)
|
||||
continuationToken = response.IsTruncated
|
||||
? response.NextContinuationToken
|
||||
: undefined
|
||||
} while (continuationToken)
|
||||
|
||||
return stats
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} bucket
|
||||
* @return {S3Client}
|
||||
|
||||
@@ -4,6 +4,5 @@ module.exports = {
|
||||
mochaFile: `reports/junit-mocha-${process.env.MOCHA_GREP}.xml`,
|
||||
includePending: true,
|
||||
jenkinsMode: true,
|
||||
output: true,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ SandboxedModule.configure({
|
||||
globals: { Buffer, Math, console, process, URL },
|
||||
sourceTransformers: {
|
||||
removeNodePrefix: function (source) {
|
||||
return source.replace(/require\((['"])node:/g, 'require($1')
|
||||
return source.replace(/require\(['"]node:/g, "require('")
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -400,56 +400,6 @@ describe('FSPersistorTests', function () {
|
||||
).to.equal(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('listDirectoryKeys', function () {
|
||||
beforeEach(async function () {
|
||||
for (const file of Object.values(files)) {
|
||||
await persistor.sendFile(location, file, '/uploads/info.txt')
|
||||
}
|
||||
})
|
||||
|
||||
it('should list directory keys', async function () {
|
||||
const keys = await persistor.listDirectoryKeys(location, 'animals')
|
||||
expect(keys).to.have.lengthOf(2)
|
||||
expect(keys).to.include(scenario.fsPath(files.wombat))
|
||||
expect(keys).to.include(scenario.fsPath(files.giraffe))
|
||||
})
|
||||
|
||||
it('should return empty array for non-existing directories', async function () {
|
||||
const keys = await persistor.listDirectoryKeys(
|
||||
location,
|
||||
'does-not-exist'
|
||||
)
|
||||
expect(keys).to.deep.equal([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('listDirectoryStats', function () {
|
||||
beforeEach(async function () {
|
||||
for (const file of Object.values(files)) {
|
||||
await persistor.sendFile(location, file, '/uploads/info.txt')
|
||||
}
|
||||
})
|
||||
|
||||
it('should list directory stats', async function () {
|
||||
const stats = await persistor.listDirectoryStats(location, 'animals')
|
||||
expect(stats).to.have.lengthOf(2)
|
||||
const keys = stats.map(s => s.key)
|
||||
expect(keys).to.include(scenario.fsPath(files.wombat))
|
||||
expect(keys).to.include(scenario.fsPath(files.giraffe))
|
||||
for (const stat of stats) {
|
||||
expect(stat.size).to.equal(localFiles['/uploads/info.txt'].length)
|
||||
}
|
||||
})
|
||||
|
||||
it('should return empty array for non-existing directories', async function () {
|
||||
const stats = await persistor.listDirectoryStats(
|
||||
location,
|
||||
'does-not-exist'
|
||||
)
|
||||
expect(stats).to.deep.equal([])
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -723,127 +723,4 @@ describe('GcsPersistorTests', function () {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('listDirectoryKeys', function () {
|
||||
describe('with valid parameters', function () {
|
||||
let keys
|
||||
|
||||
beforeEach(async function () {
|
||||
const filesWithNames = files.map((file, i) => ({
|
||||
...file,
|
||||
name: i === 0 ? 'llama' : 'hippo',
|
||||
}))
|
||||
GcsBucket.getFiles.resolves([filesWithNames])
|
||||
keys = await GcsPersistor.listDirectoryKeys(bucket, key)
|
||||
})
|
||||
|
||||
it('should list the objects in the directory', function () {
|
||||
expect(Storage.prototype.bucket).to.have.been.calledWith(bucket)
|
||||
expect(GcsBucket.getFiles).to.have.been.calledWith({
|
||||
prefix: `${key}/`,
|
||||
})
|
||||
})
|
||||
|
||||
it('should return the keys', function () {
|
||||
expect(keys).to.deep.equal(['llama', 'hippo'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('when there are no files', function () {
|
||||
let keys
|
||||
|
||||
beforeEach(async function () {
|
||||
GcsBucket.getFiles.resolves([[]])
|
||||
keys = await GcsPersistor.listDirectoryKeys(bucket, key)
|
||||
})
|
||||
|
||||
it('should return an empty array', function () {
|
||||
expect(keys).to.deep.equal([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('when there is an error listing the objects', function () {
|
||||
let error
|
||||
|
||||
beforeEach(async function () {
|
||||
GcsBucket.getFiles.rejects(genericError)
|
||||
try {
|
||||
await GcsPersistor.listDirectoryKeys(bucket, key)
|
||||
} catch (err) {
|
||||
error = err
|
||||
}
|
||||
})
|
||||
|
||||
it('should generate a ReadError', function () {
|
||||
expect(error).to.be.an.instanceOf(Errors.ReadError)
|
||||
})
|
||||
|
||||
it('should wrap the error', function () {
|
||||
expect(error.cause).to.equal(genericError)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('listDirectoryStats', function () {
|
||||
describe('with valid parameters', function () {
|
||||
let stats
|
||||
|
||||
beforeEach(async function () {
|
||||
const filesWithNames = files.map((file, i) => ({
|
||||
...file,
|
||||
name: i === 0 ? 'llama' : 'hippo',
|
||||
}))
|
||||
GcsBucket.getFiles.resolves([filesWithNames])
|
||||
stats = await GcsPersistor.listDirectoryStats(bucket, key)
|
||||
})
|
||||
|
||||
it('should list the objects in the directory', function () {
|
||||
expect(Storage.prototype.bucket).to.have.been.calledWith(bucket)
|
||||
expect(GcsBucket.getFiles).to.have.been.calledWith({
|
||||
prefix: `${key}/`,
|
||||
})
|
||||
})
|
||||
|
||||
it('should return the stats', function () {
|
||||
expect(stats).to.deep.equal([
|
||||
{ key: 'llama', size: 11 },
|
||||
{ key: 'hippo', size: 22 },
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('when there are no files', function () {
|
||||
let stats
|
||||
|
||||
beforeEach(async function () {
|
||||
GcsBucket.getFiles.resolves([[]])
|
||||
stats = await GcsPersistor.listDirectoryStats(bucket, key)
|
||||
})
|
||||
|
||||
it('should return an empty array', function () {
|
||||
expect(stats).to.deep.equal([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('when there is an error listing the objects', function () {
|
||||
let error
|
||||
|
||||
beforeEach(async function () {
|
||||
GcsBucket.getFiles.rejects(genericError)
|
||||
try {
|
||||
await GcsPersistor.listDirectoryStats(bucket, key)
|
||||
} catch (err) {
|
||||
error = err
|
||||
}
|
||||
})
|
||||
|
||||
it('should generate a ReadError', function () {
|
||||
expect(error).to.be.an.instanceOf(Errors.ReadError)
|
||||
})
|
||||
|
||||
it('should wrap the error', function () {
|
||||
expect(error.cause).to.equal(genericError)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1046,215 +1046,4 @@ describe('S3PersistorTests', function () {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('listDirectoryKeys', function () {
|
||||
describe('with valid parameters', function () {
|
||||
let keys
|
||||
|
||||
beforeEach(async function () {
|
||||
S3.mockSend(S3.ListObjectsV2Command, {
|
||||
Contents: files.map(file => ({ Key: file.Key })),
|
||||
IsTruncated: false,
|
||||
})
|
||||
keys = await S3Persistor.listDirectoryKeys(bucket, key)
|
||||
})
|
||||
|
||||
it('should list the objects in the directory', function () {
|
||||
S3.assertSendCalledWith(S3.ListObjectsV2Command, {
|
||||
Bucket: bucket,
|
||||
Prefix: key,
|
||||
})
|
||||
})
|
||||
|
||||
it('should return the keys', function () {
|
||||
expect(keys).to.deep.equal(['llama', 'hippo'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('when there are no files', function () {
|
||||
let keys
|
||||
|
||||
beforeEach(async function () {
|
||||
S3.mockSend(S3.ListObjectsV2Command, {
|
||||
Contents: [],
|
||||
IsTruncated: false,
|
||||
})
|
||||
keys = await S3Persistor.listDirectoryKeys(bucket, key)
|
||||
})
|
||||
|
||||
it('should return an empty array', function () {
|
||||
expect(keys).to.deep.equal([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('when there are more files available', function () {
|
||||
const continuationToken = 'wombat'
|
||||
let keys
|
||||
|
||||
beforeEach(async function () {
|
||||
S3.mockSend(
|
||||
S3.ListObjectsV2Command,
|
||||
{
|
||||
Contents: [files[0]].map(file => ({ Key: file.Key })),
|
||||
IsTruncated: true,
|
||||
NextContinuationToken: continuationToken,
|
||||
},
|
||||
{
|
||||
nextResponses: [
|
||||
{
|
||||
Contents: [files[1]].map(file => ({ Key: file.Key })),
|
||||
IsTruncated: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
keys = await S3Persistor.listDirectoryKeys(bucket, key)
|
||||
})
|
||||
|
||||
it('should list the objects in multiple calls', function () {
|
||||
S3.assertSendCallCount(S3.ListObjectsV2Command, 2)
|
||||
expect(S3.s3ClientStub.send.firstCall.args[0].payload).to.deep.equal({
|
||||
Bucket: bucket,
|
||||
Prefix: key,
|
||||
})
|
||||
expect(S3.s3ClientStub.send.secondCall.args[0].payload).to.deep.equal({
|
||||
Bucket: bucket,
|
||||
Prefix: key,
|
||||
ContinuationToken: continuationToken,
|
||||
})
|
||||
})
|
||||
|
||||
it('should return all keys', function () {
|
||||
expect(keys).to.deep.equal(['llama', 'hippo'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('when there is an error listing the objects', function () {
|
||||
let error
|
||||
|
||||
beforeEach(async function () {
|
||||
S3.mockSend(S3.ListObjectsV2Command, genericError, { rejects: true })
|
||||
try {
|
||||
await S3Persistor.listDirectoryKeys(bucket, key)
|
||||
} catch (err) {
|
||||
error = err
|
||||
}
|
||||
})
|
||||
|
||||
it('should generate a ReadError', function () {
|
||||
expect(error).to.be.an.instanceOf(Errors.ReadError)
|
||||
})
|
||||
|
||||
it('should wrap the error', function () {
|
||||
expect(error.cause).to.exist
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('listDirectoryStats', function () {
|
||||
describe('with valid parameters', function () {
|
||||
let stats
|
||||
|
||||
beforeEach(async function () {
|
||||
S3.mockSend(S3.ListObjectsV2Command, {
|
||||
Contents: files.map(file => ({ Key: file.Key, Size: file.Size })),
|
||||
IsTruncated: false,
|
||||
})
|
||||
stats = await S3Persistor.listDirectoryStats(bucket, key)
|
||||
})
|
||||
|
||||
it('should list the objects in the directory', function () {
|
||||
S3.assertSendCalledWith(S3.ListObjectsV2Command, {
|
||||
Bucket: bucket,
|
||||
Prefix: key,
|
||||
})
|
||||
})
|
||||
|
||||
it('should return the stats', function () {
|
||||
expect(stats).to.deep.equal([
|
||||
{ key: 'llama', size: 11 },
|
||||
{ key: 'hippo', size: 22 },
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('when there are no files', function () {
|
||||
let stats
|
||||
|
||||
beforeEach(async function () {
|
||||
S3.mockSend(S3.ListObjectsV2Command, {
|
||||
Contents: [],
|
||||
IsTruncated: false,
|
||||
})
|
||||
stats = await S3Persistor.listDirectoryStats(bucket, key)
|
||||
})
|
||||
|
||||
it('should return an empty array', function () {
|
||||
expect(stats).to.deep.equal([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('when there are more files available', function () {
|
||||
const continuationToken = 'wombat'
|
||||
let stats
|
||||
|
||||
beforeEach(async function () {
|
||||
S3.mockSend(
|
||||
S3.ListObjectsV2Command,
|
||||
{
|
||||
Contents: [files[0]].map(file => ({
|
||||
Key: file.Key,
|
||||
Size: file.Size,
|
||||
})),
|
||||
IsTruncated: true,
|
||||
NextContinuationToken: continuationToken,
|
||||
},
|
||||
{
|
||||
nextResponses: [
|
||||
{
|
||||
Contents: [files[1]].map(file => ({
|
||||
Key: file.Key,
|
||||
Size: file.Size,
|
||||
})),
|
||||
IsTruncated: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
stats = await S3Persistor.listDirectoryStats(bucket, key)
|
||||
})
|
||||
|
||||
it('should list the objects in multiple calls', function () {
|
||||
S3.assertSendCallCount(S3.ListObjectsV2Command, 2)
|
||||
})
|
||||
|
||||
it('should return all stats', function () {
|
||||
expect(stats).to.deep.equal([
|
||||
{ key: 'llama', size: 11 },
|
||||
{ key: 'hippo', size: 22 },
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('when there is an error listing the objects', function () {
|
||||
let error
|
||||
|
||||
beforeEach(async function () {
|
||||
S3.mockSend(S3.ListObjectsV2Command, genericError, { rejects: true })
|
||||
try {
|
||||
await S3Persistor.listDirectoryStats(bucket, key)
|
||||
} catch (err) {
|
||||
error = err
|
||||
}
|
||||
})
|
||||
|
||||
it('should generate a ReadError', function () {
|
||||
expect(error).to.be.an.instanceOf(Errors.ReadError)
|
||||
})
|
||||
|
||||
it('should wrap the error', function () {
|
||||
expect(error.cause).to.exist
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# Autogenerated by build scripts. Do not edit.
|
||||
.eslint*
|
||||
.prettier*
|
||||
libraries/o-error/**
|
||||
libraries/overleaf-editor-core/**
|
||||
package-lock.json
|
||||
package.json
|
||||
patches/**
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
// Autogenerated by build scripts. Do not edit.
|
||||
|
||||
pipeline {
|
||||
agent {
|
||||
node {
|
||||
label 'jenkins-agent-web'
|
||||
customWorkspace '/workspace'
|
||||
}
|
||||
}
|
||||
options {
|
||||
timestamps()
|
||||
timeout(time: 15, unit: 'MINUTES')
|
||||
}
|
||||
environment {
|
||||
BRANCH_NAME = "${env.CHANGE_BRANCH ? env.CHANGE_BRANCH : env.BRANCH_NAME}"
|
||||
JENKINS_BUILD_NUMBER = "${BUILD_NUMBER}"
|
||||
BUILD_NUMBER = "${SHORT_SHA}_${BUILD_NUMBER}"
|
||||
COMMIT_SHA = "${GIT_COMMIT}"
|
||||
SHORT_SHA = "${GIT_COMMIT.take(7)}"
|
||||
}
|
||||
stages {
|
||||
stage('Stage 1') {
|
||||
parallel {
|
||||
stage('Install monorepo') {
|
||||
steps {
|
||||
retry(count: 3) {
|
||||
sh 'make monorepo_setup'
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Create reports folder') {
|
||||
steps {
|
||||
sh 'mkdir libraries/overleaf-editor-core/reports'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Stage 2') {
|
||||
parallel {
|
||||
stage('Lint') {
|
||||
steps {
|
||||
sh 'bin/run -w /overleaf/libraries/overleaf-editor-core monorepo npm run lint -- --format json --output-file reports/eslint.json'
|
||||
}
|
||||
post {
|
||||
always {
|
||||
sh """
|
||||
sed -i 's_"filePath":"/overleaf_"filePath":"/workspace_g' libraries/overleaf-editor-core/reports/eslint.json
|
||||
"""
|
||||
recordIssues checksAnnotationScope: 'ALL', enabledForFailure: true, failOnError: true, id: 'overleaf-editor-core-eslint', name: 'overleaf-editor-core eslint', qualityGates: [[integerThreshold: 1, threshold: 1.0, type: 'TOTAL']], sourceCodeRetention: 'LAST_BUILD', tools: [esLint(pattern: 'libraries/overleaf-editor-core/reports/eslint.json')]
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Format') {
|
||||
steps {
|
||||
sh 'bin/run -w /overleaf/libraries/overleaf-editor-core monorepo npm run format'
|
||||
}
|
||||
}
|
||||
stage('Typecheck') {
|
||||
steps {
|
||||
sh 'bin/run -w /overleaf/libraries/overleaf-editor-core monorepo npm run types:check'
|
||||
}
|
||||
}
|
||||
stage('Test') {
|
||||
steps {
|
||||
retry(count: 3) {
|
||||
sh 'bin/run -w /overleaf/libraries/overleaf-editor-core monorepo npm run test:ci'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
post {
|
||||
always {
|
||||
junit checksName: 'overleaf-editor-core test results', testResults: 'libraries/overleaf-editor-core/reports/junit-*.xml'
|
||||
}
|
||||
cleanup {
|
||||
sh 'rm -rf libraries/overleaf-editor-core/reports'
|
||||
sh 'make clean_jenkins -j10'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,5 +5,4 @@ overleaf-editor-core
|
||||
--esmock-loader=False
|
||||
--is-library=True
|
||||
--node-version=22.18.0
|
||||
--pipeline-owner=🥑 Core
|
||||
--public-repo=False
|
||||
|
||||
@@ -4,6 +4,5 @@ module.exports = {
|
||||
mochaFile: `reports/junit-mocha-${process.env.MOCHA_GREP}.xml`,
|
||||
includePending: true,
|
||||
jenkinsMode: true,
|
||||
output: true,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# Autogenerated by build scripts. Do not edit.
|
||||
.eslint*
|
||||
.prettier*
|
||||
libraries/promise-utils/**
|
||||
package-lock.json
|
||||
package.json
|
||||
patches/**
|
||||
Vendored
+82
@@ -0,0 +1,82 @@
|
||||
// Autogenerated by build scripts. Do not edit.
|
||||
|
||||
pipeline {
|
||||
agent {
|
||||
node {
|
||||
label 'jenkins-agent-web'
|
||||
customWorkspace '/workspace'
|
||||
}
|
||||
}
|
||||
options {
|
||||
timestamps()
|
||||
timeout(time: 15, unit: 'MINUTES')
|
||||
}
|
||||
environment {
|
||||
BRANCH_NAME = "${env.CHANGE_BRANCH ? env.CHANGE_BRANCH : env.BRANCH_NAME}"
|
||||
JENKINS_BUILD_NUMBER = "${BUILD_NUMBER}"
|
||||
BUILD_NUMBER = "${SHORT_SHA}_${BUILD_NUMBER}"
|
||||
COMMIT_SHA = "${GIT_COMMIT}"
|
||||
SHORT_SHA = "${GIT_COMMIT.take(7)}"
|
||||
}
|
||||
stages {
|
||||
stage('Stage 1') {
|
||||
parallel {
|
||||
stage('Install monorepo') {
|
||||
steps {
|
||||
retry(count: 3) {
|
||||
sh 'make monorepo_setup'
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Create reports folder') {
|
||||
steps {
|
||||
sh 'mkdir libraries/promise-utils/reports'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Stage 2') {
|
||||
parallel {
|
||||
stage('Lint') {
|
||||
steps {
|
||||
sh 'bin/run -w /overleaf/libraries/promise-utils monorepo npm run lint -- --format json --output-file reports/eslint.json'
|
||||
}
|
||||
post {
|
||||
always {
|
||||
sh """
|
||||
sed -i 's_"filePath":"/overleaf_"filePath":"/workspace_g' libraries/promise-utils/reports/eslint.json
|
||||
"""
|
||||
recordIssues checksAnnotationScope: 'ALL', enabledForFailure: true, failOnError: true, id: 'promise-utils-eslint', name: 'promise-utils eslint', qualityGates: [[integerThreshold: 1, threshold: 1.0, type: 'TOTAL']], sourceCodeRetention: 'LAST_BUILD', tools: [esLint(pattern: 'libraries/promise-utils/reports/eslint.json')]
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Format') {
|
||||
steps {
|
||||
sh 'bin/run -w /overleaf/libraries/promise-utils monorepo npm run format'
|
||||
}
|
||||
}
|
||||
stage('Typecheck') {
|
||||
steps {
|
||||
sh 'bin/run -w /overleaf/libraries/promise-utils monorepo npm run types:check'
|
||||
}
|
||||
}
|
||||
stage('Test') {
|
||||
steps {
|
||||
retry(count: 3) {
|
||||
sh 'bin/run -w /overleaf/libraries/promise-utils monorepo npm run test:ci'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
post {
|
||||
always {
|
||||
junit checksName: 'promise-utils test results', testResults: 'libraries/promise-utils/reports/junit-*.xml'
|
||||
}
|
||||
cleanup {
|
||||
sh 'rm -rf libraries/promise-utils/reports'
|
||||
sh 'make clean_jenkins -j10'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,5 +5,4 @@ promise-utils
|
||||
--esmock-loader=False
|
||||
--is-library=True
|
||||
--node-version=22.18.0
|
||||
--pipeline-owner=🚉 Platform
|
||||
--public-repo=False
|
||||
|
||||
@@ -4,6 +4,5 @@ module.exports = {
|
||||
mochaFile: `reports/junit-mocha-${process.env.MOCHA_GREP}.xml`,
|
||||
includePending: true,
|
||||
jenkinsMode: true,
|
||||
output: true,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# Autogenerated by build scripts. Do not edit.
|
||||
.eslint*
|
||||
.prettier*
|
||||
libraries/ranges-tracker/**
|
||||
package-lock.json
|
||||
package.json
|
||||
patches/**
|
||||
Vendored
+82
@@ -0,0 +1,82 @@
|
||||
// Autogenerated by build scripts. Do not edit.
|
||||
|
||||
pipeline {
|
||||
agent {
|
||||
node {
|
||||
label 'jenkins-agent-web'
|
||||
customWorkspace '/workspace'
|
||||
}
|
||||
}
|
||||
options {
|
||||
timestamps()
|
||||
timeout(time: 15, unit: 'MINUTES')
|
||||
}
|
||||
environment {
|
||||
BRANCH_NAME = "${env.CHANGE_BRANCH ? env.CHANGE_BRANCH : env.BRANCH_NAME}"
|
||||
JENKINS_BUILD_NUMBER = "${BUILD_NUMBER}"
|
||||
BUILD_NUMBER = "${SHORT_SHA}_${BUILD_NUMBER}"
|
||||
COMMIT_SHA = "${GIT_COMMIT}"
|
||||
SHORT_SHA = "${GIT_COMMIT.take(7)}"
|
||||
}
|
||||
stages {
|
||||
stage('Stage 1') {
|
||||
parallel {
|
||||
stage('Install monorepo') {
|
||||
steps {
|
||||
retry(count: 3) {
|
||||
sh 'make monorepo_setup'
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Create reports folder') {
|
||||
steps {
|
||||
sh 'mkdir libraries/ranges-tracker/reports'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Stage 2') {
|
||||
parallel {
|
||||
stage('Lint') {
|
||||
steps {
|
||||
sh 'bin/run -w /overleaf/libraries/ranges-tracker monorepo npm run lint -- --format json --output-file reports/eslint.json'
|
||||
}
|
||||
post {
|
||||
always {
|
||||
sh """
|
||||
sed -i 's_"filePath":"/overleaf_"filePath":"/workspace_g' libraries/ranges-tracker/reports/eslint.json
|
||||
"""
|
||||
recordIssues checksAnnotationScope: 'ALL', enabledForFailure: true, failOnError: true, id: 'ranges-tracker-eslint', name: 'ranges-tracker eslint', qualityGates: [[integerThreshold: 1, threshold: 1.0, type: 'TOTAL']], sourceCodeRetention: 'LAST_BUILD', tools: [esLint(pattern: 'libraries/ranges-tracker/reports/eslint.json')]
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Format') {
|
||||
steps {
|
||||
sh 'bin/run -w /overleaf/libraries/ranges-tracker monorepo npm run format'
|
||||
}
|
||||
}
|
||||
stage('Typecheck') {
|
||||
steps {
|
||||
sh 'bin/run -w /overleaf/libraries/ranges-tracker monorepo npm run types:check'
|
||||
}
|
||||
}
|
||||
stage('Test') {
|
||||
steps {
|
||||
retry(count: 3) {
|
||||
sh 'bin/run -w /overleaf/libraries/ranges-tracker monorepo npm run test:ci'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
post {
|
||||
always {
|
||||
junit checksName: 'ranges-tracker test results', testResults: 'libraries/ranges-tracker/reports/junit-*.xml'
|
||||
}
|
||||
cleanup {
|
||||
sh 'rm -rf libraries/ranges-tracker/reports'
|
||||
sh 'make clean_jenkins -j10'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,5 +5,4 @@ ranges-tracker
|
||||
--esmock-loader=False
|
||||
--is-library=True
|
||||
--node-version=22.18.0
|
||||
--pipeline-owner=🥑 Core
|
||||
--public-repo=False
|
||||
|
||||
@@ -4,6 +4,5 @@ module.exports = {
|
||||
mochaFile: `reports/junit-mocha-${process.env.MOCHA_GREP}.xml`,
|
||||
includePending: true,
|
||||
jenkinsMode: true,
|
||||
output: true,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# Autogenerated by build scripts. Do not edit.
|
||||
.eslint*
|
||||
.prettier*
|
||||
libraries/fetch-utils/**
|
||||
libraries/logger/**
|
||||
libraries/o-error/**
|
||||
libraries/redis-wrapper/**
|
||||
package-lock.json
|
||||
package.json
|
||||
patches/**
|
||||
Vendored
+82
@@ -0,0 +1,82 @@
|
||||
// Autogenerated by build scripts. Do not edit.
|
||||
|
||||
pipeline {
|
||||
agent {
|
||||
node {
|
||||
label 'jenkins-agent-web'
|
||||
customWorkspace '/workspace'
|
||||
}
|
||||
}
|
||||
options {
|
||||
timestamps()
|
||||
timeout(time: 15, unit: 'MINUTES')
|
||||
}
|
||||
environment {
|
||||
BRANCH_NAME = "${env.CHANGE_BRANCH ? env.CHANGE_BRANCH : env.BRANCH_NAME}"
|
||||
JENKINS_BUILD_NUMBER = "${BUILD_NUMBER}"
|
||||
BUILD_NUMBER = "${SHORT_SHA}_${BUILD_NUMBER}"
|
||||
COMMIT_SHA = "${GIT_COMMIT}"
|
||||
SHORT_SHA = "${GIT_COMMIT.take(7)}"
|
||||
}
|
||||
stages {
|
||||
stage('Stage 1') {
|
||||
parallel {
|
||||
stage('Install monorepo') {
|
||||
steps {
|
||||
retry(count: 3) {
|
||||
sh 'make monorepo_setup'
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Create reports folder') {
|
||||
steps {
|
||||
sh 'mkdir libraries/redis-wrapper/reports'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Stage 2') {
|
||||
parallel {
|
||||
stage('Lint') {
|
||||
steps {
|
||||
sh 'bin/run -w /overleaf/libraries/redis-wrapper monorepo npm run lint -- --format json --output-file reports/eslint.json'
|
||||
}
|
||||
post {
|
||||
always {
|
||||
sh """
|
||||
sed -i 's_"filePath":"/overleaf_"filePath":"/workspace_g' libraries/redis-wrapper/reports/eslint.json
|
||||
"""
|
||||
recordIssues checksAnnotationScope: 'ALL', enabledForFailure: true, failOnError: true, id: 'redis-wrapper-eslint', name: 'redis-wrapper eslint', qualityGates: [[integerThreshold: 1, threshold: 1.0, type: 'TOTAL']], sourceCodeRetention: 'LAST_BUILD', tools: [esLint(pattern: 'libraries/redis-wrapper/reports/eslint.json')]
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Format') {
|
||||
steps {
|
||||
sh 'bin/run -w /overleaf/libraries/redis-wrapper monorepo npm run format'
|
||||
}
|
||||
}
|
||||
stage('Typecheck') {
|
||||
steps {
|
||||
sh 'bin/run -w /overleaf/libraries/redis-wrapper monorepo npm run types:check'
|
||||
}
|
||||
}
|
||||
stage('Test') {
|
||||
steps {
|
||||
retry(count: 3) {
|
||||
sh 'bin/run -w /overleaf/libraries/redis-wrapper monorepo npm run test:ci'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
post {
|
||||
always {
|
||||
junit checksName: 'redis-wrapper test results', testResults: 'libraries/redis-wrapper/reports/junit-*.xml'
|
||||
}
|
||||
cleanup {
|
||||
sh 'rm -rf libraries/redis-wrapper/reports'
|
||||
sh 'make clean_jenkins -j10'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,5 +5,4 @@ redis-wrapper
|
||||
--esmock-loader=False
|
||||
--is-library=True
|
||||
--node-version=22.18.0
|
||||
--pipeline-owner=🚉 Platform
|
||||
--public-repo=False
|
||||
|
||||
@@ -4,6 +4,5 @@ module.exports = {
|
||||
mochaFile: `reports/junit-mocha-${process.env.MOCHA_GREP}.xml`,
|
||||
includePending: true,
|
||||
jenkinsMode: true,
|
||||
output: true,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# Autogenerated by build scripts. Do not edit.
|
||||
.eslint*
|
||||
.prettier*
|
||||
libraries/settings/**
|
||||
package-lock.json
|
||||
package.json
|
||||
patches/**
|
||||
Vendored
+72
@@ -0,0 +1,72 @@
|
||||
// Autogenerated by build scripts. Do not edit.
|
||||
|
||||
pipeline {
|
||||
agent {
|
||||
node {
|
||||
label 'jenkins-agent-web'
|
||||
customWorkspace '/workspace'
|
||||
}
|
||||
}
|
||||
options {
|
||||
timestamps()
|
||||
timeout(time: 15, unit: 'MINUTES')
|
||||
}
|
||||
environment {
|
||||
BRANCH_NAME = "${env.CHANGE_BRANCH ? env.CHANGE_BRANCH : env.BRANCH_NAME}"
|
||||
JENKINS_BUILD_NUMBER = "${BUILD_NUMBER}"
|
||||
BUILD_NUMBER = "${SHORT_SHA}_${BUILD_NUMBER}"
|
||||
COMMIT_SHA = "${GIT_COMMIT}"
|
||||
SHORT_SHA = "${GIT_COMMIT.take(7)}"
|
||||
}
|
||||
stages {
|
||||
stage('Stage 1') {
|
||||
parallel {
|
||||
stage('Install monorepo') {
|
||||
steps {
|
||||
retry(count: 3) {
|
||||
sh 'make monorepo_setup'
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Create reports folder') {
|
||||
steps {
|
||||
sh 'mkdir libraries/settings/reports'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Stage 2') {
|
||||
parallel {
|
||||
stage('Lint') {
|
||||
steps {
|
||||
sh 'bin/run -w /overleaf/libraries/settings monorepo npm run lint -- --format json --output-file reports/eslint.json'
|
||||
}
|
||||
post {
|
||||
always {
|
||||
sh """
|
||||
sed -i 's_"filePath":"/overleaf_"filePath":"/workspace_g' libraries/settings/reports/eslint.json
|
||||
"""
|
||||
recordIssues checksAnnotationScope: 'ALL', enabledForFailure: true, failOnError: true, id: 'settings-eslint', name: 'settings eslint', qualityGates: [[integerThreshold: 1, threshold: 1.0, type: 'TOTAL']], sourceCodeRetention: 'LAST_BUILD', tools: [esLint(pattern: 'libraries/settings/reports/eslint.json')]
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Format') {
|
||||
steps {
|
||||
sh 'bin/run -w /overleaf/libraries/settings monorepo npm run format'
|
||||
}
|
||||
}
|
||||
stage('Typecheck') {
|
||||
steps {
|
||||
sh 'bin/run -w /overleaf/libraries/settings monorepo npm run types:check'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
post {
|
||||
cleanup {
|
||||
sh 'rm -rf libraries/settings/reports'
|
||||
sh 'make clean_jenkins -j10'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,5 +5,4 @@ settings
|
||||
--esmock-loader=False
|
||||
--is-library=True
|
||||
--node-version=22.18.0
|
||||
--pipeline-owner=🚉 Platform
|
||||
--public-repo=False
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# Autogenerated by build scripts. Do not edit.
|
||||
.eslint*
|
||||
.prettier*
|
||||
libraries/stream-utils/**
|
||||
package-lock.json
|
||||
package.json
|
||||
patches/**
|
||||
Vendored
+82
@@ -0,0 +1,82 @@
|
||||
// Autogenerated by build scripts. Do not edit.
|
||||
|
||||
pipeline {
|
||||
agent {
|
||||
node {
|
||||
label 'jenkins-agent-web'
|
||||
customWorkspace '/workspace'
|
||||
}
|
||||
}
|
||||
options {
|
||||
timestamps()
|
||||
timeout(time: 15, unit: 'MINUTES')
|
||||
}
|
||||
environment {
|
||||
BRANCH_NAME = "${env.CHANGE_BRANCH ? env.CHANGE_BRANCH : env.BRANCH_NAME}"
|
||||
JENKINS_BUILD_NUMBER = "${BUILD_NUMBER}"
|
||||
BUILD_NUMBER = "${SHORT_SHA}_${BUILD_NUMBER}"
|
||||
COMMIT_SHA = "${GIT_COMMIT}"
|
||||
SHORT_SHA = "${GIT_COMMIT.take(7)}"
|
||||
}
|
||||
stages {
|
||||
stage('Stage 1') {
|
||||
parallel {
|
||||
stage('Install monorepo') {
|
||||
steps {
|
||||
retry(count: 3) {
|
||||
sh 'make monorepo_setup'
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Create reports folder') {
|
||||
steps {
|
||||
sh 'mkdir libraries/stream-utils/reports'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Stage 2') {
|
||||
parallel {
|
||||
stage('Lint') {
|
||||
steps {
|
||||
sh 'bin/run -w /overleaf/libraries/stream-utils monorepo npm run lint -- --format json --output-file reports/eslint.json'
|
||||
}
|
||||
post {
|
||||
always {
|
||||
sh """
|
||||
sed -i 's_"filePath":"/overleaf_"filePath":"/workspace_g' libraries/stream-utils/reports/eslint.json
|
||||
"""
|
||||
recordIssues checksAnnotationScope: 'ALL', enabledForFailure: true, failOnError: true, id: 'stream-utils-eslint', name: 'stream-utils eslint', qualityGates: [[integerThreshold: 1, threshold: 1.0, type: 'TOTAL']], sourceCodeRetention: 'LAST_BUILD', tools: [esLint(pattern: 'libraries/stream-utils/reports/eslint.json')]
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Format') {
|
||||
steps {
|
||||
sh 'bin/run -w /overleaf/libraries/stream-utils monorepo npm run format'
|
||||
}
|
||||
}
|
||||
stage('Typecheck') {
|
||||
steps {
|
||||
sh 'bin/run -w /overleaf/libraries/stream-utils monorepo npm run types:check'
|
||||
}
|
||||
}
|
||||
stage('Test') {
|
||||
steps {
|
||||
retry(count: 3) {
|
||||
sh 'bin/run -w /overleaf/libraries/stream-utils monorepo npm run test:ci'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
post {
|
||||
always {
|
||||
junit checksName: 'stream-utils test results', testResults: 'libraries/stream-utils/reports/junit-*.xml'
|
||||
}
|
||||
cleanup {
|
||||
sh 'rm -rf libraries/stream-utils/reports'
|
||||
sh 'make clean_jenkins -j10'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,5 +5,4 @@ stream-utils
|
||||
--esmock-loader=False
|
||||
--is-library=True
|
||||
--node-version=22.18.0
|
||||
--pipeline-owner=🚉 Platform
|
||||
--public-repo=False
|
||||
|
||||
@@ -4,6 +4,5 @@ module.exports = {
|
||||
mochaFile: `reports/junit-mocha-${process.env.MOCHA_GREP}.xml`,
|
||||
includePending: true,
|
||||
jenkinsMode: true,
|
||||
output: true,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# Autogenerated by build scripts. Do not edit.
|
||||
.eslint*
|
||||
.prettier*
|
||||
libraries/o-error/**
|
||||
libraries/validation-tools/**
|
||||
package-lock.json
|
||||
package.json
|
||||
patches/**
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
// Autogenerated by build scripts. Do not edit.
|
||||
|
||||
pipeline {
|
||||
agent {
|
||||
node {
|
||||
label 'jenkins-agent-web'
|
||||
customWorkspace '/workspace'
|
||||
}
|
||||
}
|
||||
options {
|
||||
timestamps()
|
||||
timeout(time: 15, unit: 'MINUTES')
|
||||
}
|
||||
environment {
|
||||
BRANCH_NAME = "${env.CHANGE_BRANCH ? env.CHANGE_BRANCH : env.BRANCH_NAME}"
|
||||
JENKINS_BUILD_NUMBER = "${BUILD_NUMBER}"
|
||||
BUILD_NUMBER = "${SHORT_SHA}_${BUILD_NUMBER}"
|
||||
COMMIT_SHA = "${GIT_COMMIT}"
|
||||
SHORT_SHA = "${GIT_COMMIT.take(7)}"
|
||||
}
|
||||
stages {
|
||||
stage('Stage 1') {
|
||||
parallel {
|
||||
stage('Install monorepo') {
|
||||
steps {
|
||||
retry(count: 3) {
|
||||
sh 'make monorepo_setup'
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Create reports folder') {
|
||||
steps {
|
||||
sh 'mkdir libraries/validation-tools/reports'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Stage 2') {
|
||||
parallel {
|
||||
stage('Lint') {
|
||||
steps {
|
||||
sh 'bin/run -w /overleaf/libraries/validation-tools monorepo npm run lint -- --format json --output-file reports/eslint.json'
|
||||
}
|
||||
post {
|
||||
always {
|
||||
sh """
|
||||
sed -i 's_"filePath":"/overleaf_"filePath":"/workspace_g' libraries/validation-tools/reports/eslint.json
|
||||
"""
|
||||
recordIssues checksAnnotationScope: 'ALL', enabledForFailure: true, failOnError: true, id: 'validation-tools-eslint', name: 'validation-tools eslint', qualityGates: [[integerThreshold: 1, threshold: 1.0, type: 'TOTAL']], sourceCodeRetention: 'LAST_BUILD', tools: [esLint(pattern: 'libraries/validation-tools/reports/eslint.json')]
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Format') {
|
||||
steps {
|
||||
sh 'bin/run -w /overleaf/libraries/validation-tools monorepo npm run format'
|
||||
}
|
||||
}
|
||||
stage('Typecheck') {
|
||||
steps {
|
||||
sh 'bin/run -w /overleaf/libraries/validation-tools monorepo npm run types:check'
|
||||
}
|
||||
}
|
||||
stage('Test') {
|
||||
steps {
|
||||
retry(count: 3) {
|
||||
sh 'bin/run -w /overleaf/libraries/validation-tools monorepo npm run test:ci'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
post {
|
||||
always {
|
||||
junit checksName: 'validation-tools test results', testResults: 'libraries/validation-tools/reports/junit-*.xml'
|
||||
}
|
||||
cleanup {
|
||||
sh 'rm -rf libraries/validation-tools/reports'
|
||||
sh 'make clean_jenkins -j10'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
const { isZodErrorLike, fromError } = require('zod-validation-error')
|
||||
|
||||
function createHandleValidationError(statusCode = 400) {
|
||||
return [
|
||||
(err, req, res, next) => {
|
||||
if (!isZodErrorLike(err)) {
|
||||
return next(err)
|
||||
}
|
||||
|
||||
res.status(statusCode).json({ ...fromError(err), statusCode })
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const handleValidationError = createHandleValidationError(400)
|
||||
|
||||
module.exports = { handleValidationError, createHandleValidationError }
|
||||
@@ -1,19 +1,11 @@
|
||||
const { ParamsError } = require('./Errors')
|
||||
const { z } = require('zod')
|
||||
const { zz } = require('./zodHelpers')
|
||||
const { parseReq } = require('./parseReq')
|
||||
const { validateSchema } = require('./validateSchema')
|
||||
const {
|
||||
handleValidationError,
|
||||
createHandleValidationError,
|
||||
} = require('./handleValidationError')
|
||||
const { validateReq } = require('./validateReq')
|
||||
|
||||
module.exports = {
|
||||
z,
|
||||
zz,
|
||||
validateSchema,
|
||||
parseReq,
|
||||
handleValidationError,
|
||||
createHandleValidationError,
|
||||
validateReq,
|
||||
ParamsError,
|
||||
}
|
||||
|
||||
@@ -22,11 +22,10 @@
|
||||
"dependencies": {
|
||||
"@overleaf/o-error": "*",
|
||||
"mongodb": "^6.12.0",
|
||||
"zod": "^4.1.8",
|
||||
"zod-validation-error": "^4.0.1"
|
||||
"zod": "^4.1.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.0.4",
|
||||
"vitest": "^4.0.0"
|
||||
"vitest": "^3.2.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { parseReq } from '../../../parseReq'
|
||||
import { validateReq } from '../../../validateReq'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { z } from 'zod'
|
||||
import type { Request } from 'express'
|
||||
import { zz } from '../../../zodHelpers'
|
||||
|
||||
describe('parseReq', () => {
|
||||
describe('validateReq', () => {
|
||||
describe('with a request that is valid for the schema', () => {
|
||||
it('should return the parsed request', () => {
|
||||
const req = {
|
||||
@@ -25,7 +25,7 @@ describe('parseReq', () => {
|
||||
}),
|
||||
})
|
||||
|
||||
const result = parseReq(req, schema)
|
||||
const result = validateReq(req, schema)
|
||||
|
||||
expect(result).toEqual({
|
||||
params: {
|
||||
@@ -46,7 +46,7 @@ describe('parseReq', () => {
|
||||
} as Request<{ id: string }>
|
||||
|
||||
expect(() =>
|
||||
parseReq(
|
||||
validateReq(
|
||||
req,
|
||||
z.object({
|
||||
params: z.object({
|
||||
@@ -65,7 +65,7 @@ describe('parseReq', () => {
|
||||
} as Request
|
||||
|
||||
expect(() =>
|
||||
parseReq(
|
||||
validateReq(
|
||||
req,
|
||||
z.object({
|
||||
body: z.object({
|
||||
|
||||
@@ -41,180 +41,4 @@ describe('zodHelpers', () => {
|
||||
expect(parsed.data?.toString()).toBe('507f1f77bcf86cd799439011')
|
||||
})
|
||||
})
|
||||
describe('datetime', () => {
|
||||
it('parses valid ISO 8601 datetime strings', () => {
|
||||
const parsed = zz.datetime().safeParse('2024-01-01T12:00:00Z')
|
||||
expect(parsed.success).toBe(true)
|
||||
expect(parsed.data).toEqual(new Date('2024-01-01T12:00:00Z'))
|
||||
})
|
||||
|
||||
it('parses a valid ISO 8601 datetime with offset', () => {
|
||||
const parsed = zz
|
||||
.datetime({ offset: true })
|
||||
.safeParse('2024-01-01T12:00:00+00:00')
|
||||
expect(parsed.success).toBe(true)
|
||||
expect(parsed.data).toEqual(new Date('2024-01-01T12:00:00+00:00'))
|
||||
})
|
||||
|
||||
it('parses a valid Date object', () => {
|
||||
const date = new Date('2024-01-01T12:00:00Z')
|
||||
const parsed = zz.datetime().safeParse(date)
|
||||
expect(parsed.success).toBe(true)
|
||||
expect(parsed.data).toEqual(date)
|
||||
})
|
||||
|
||||
it('fails to parse datetime with offset when offset option is false', () => {
|
||||
const parsed = zz
|
||||
.datetime({ offset: false })
|
||||
.safeParse('2024-01-01T12:00:00+00:00')
|
||||
expect(parsed.success).toBe(false)
|
||||
expect(parsed.error?.issues).toHaveLength(1)
|
||||
expect(parsed.error?.issues).toMatchObject([
|
||||
expect.objectContaining({
|
||||
code: 'invalid_format',
|
||||
format: 'datetime',
|
||||
message: 'Invalid ISO datetime',
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it('fails to parse null when schema is not nullable', () => {
|
||||
const parsed = zz.datetime().safeParse(null)
|
||||
expect(parsed.success).toBe(false)
|
||||
expect(parsed.error?.message).toContain(
|
||||
'Invalid input: expected date, received null'
|
||||
)
|
||||
})
|
||||
|
||||
it('fails to parse invalid datetime strings', () => {
|
||||
const parsed = zz.datetime().safeParse('invalid-datetime')
|
||||
expect(parsed.success).toBe(false)
|
||||
expect(parsed.error?.issues).toHaveLength(1)
|
||||
expect(parsed.error?.issues).toMatchObject([
|
||||
expect.objectContaining({
|
||||
code: 'invalid_format',
|
||||
format: 'datetime',
|
||||
message: 'Invalid ISO datetime',
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
describe('datetimeNullable', () => {
|
||||
it('parses valid ISO 8601 datetime strings', () => {
|
||||
const parsed = zz.datetimeNullable().safeParse('2024-01-01T12:00:00Z')
|
||||
expect(parsed.success).toBe(true)
|
||||
expect(parsed.data).toEqual(new Date('2024-01-01T12:00:00Z'))
|
||||
})
|
||||
|
||||
it('parses a valid ISO 8601 datetime with offset', () => {
|
||||
const parsed = zz
|
||||
.datetimeNullable({ offset: true })
|
||||
.safeParse('2024-01-01T12:00:00+00:00')
|
||||
expect(parsed.success).toBe(true)
|
||||
expect(parsed.data).toEqual(new Date('2024-01-01T12:00:00+00:00'))
|
||||
})
|
||||
|
||||
it('parses a valid Date object', () => {
|
||||
const date = new Date('2024-01-01T12:00:00Z')
|
||||
const parsed = zz.datetimeNullable().safeParse(date)
|
||||
expect(parsed.success).toBe(true)
|
||||
expect(parsed.data).toEqual(date)
|
||||
})
|
||||
|
||||
it('fails to parse datetime with offset when offset option is false', () => {
|
||||
const parsed = zz
|
||||
.datetimeNullable({ offset: false })
|
||||
.safeParse('2024-01-01T12:00:00+00:00')
|
||||
expect(parsed.success).toBe(false)
|
||||
expect(parsed.error?.issues).toHaveLength(1)
|
||||
expect(parsed.error?.issues).toMatchObject([
|
||||
expect.objectContaining({
|
||||
code: 'invalid_format',
|
||||
format: 'datetime',
|
||||
message: 'Invalid ISO datetime',
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it('parses null when schema is nullable and input is null', () => {
|
||||
const parsed = zz.datetimeNullable().safeParse(null)
|
||||
expect(parsed.success).toBe(true)
|
||||
expect(parsed.data).toBeNull()
|
||||
})
|
||||
|
||||
it('fails to parse invalid datetime strings', () => {
|
||||
const parsed = zz.datetimeNullable().safeParse('invalid-datetime')
|
||||
expect(parsed.success).toBe(false)
|
||||
expect(parsed.error?.issues).toHaveLength(1)
|
||||
expect(parsed.error?.issues).toMatchObject([
|
||||
expect.objectContaining({
|
||||
code: 'invalid_format',
|
||||
format: 'datetime',
|
||||
message: 'Invalid ISO datetime',
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
describe('datetimeNullish', () => {
|
||||
it('parses valid ISO 8601 datetime strings', () => {
|
||||
const parsed = zz.datetimeNullish().safeParse('2024-01-01T12:00:00Z')
|
||||
expect(parsed.success).toBe(true)
|
||||
expect(parsed.data).toEqual(new Date('2024-01-01T12:00:00Z'))
|
||||
})
|
||||
|
||||
it('parses a valid ISO 8601 datetime with offset', () => {
|
||||
const parsed = zz
|
||||
.datetimeNullish({ offset: true })
|
||||
.safeParse('2024-01-01T12:00:00+00:00')
|
||||
expect(parsed.success).toBe(true)
|
||||
expect(parsed.data).toEqual(new Date('2024-01-01T12:00:00+00:00'))
|
||||
})
|
||||
|
||||
it('parses a valid Date object', () => {
|
||||
const date = new Date('2024-01-01T12:00:00Z')
|
||||
const parsed = zz.datetimeNullish().safeParse(date)
|
||||
expect(parsed.success).toBe(true)
|
||||
expect(parsed.data).toEqual(date)
|
||||
})
|
||||
|
||||
it('parses null when schema is nullable and input is null', () => {
|
||||
const parsed = zz.datetimeNullish().safeParse(null)
|
||||
expect(parsed.success).toBe(true)
|
||||
expect(parsed.data).toBeNull()
|
||||
})
|
||||
|
||||
it('parses undefined when schema is nullish and input is undefined', () => {
|
||||
const parsed = zz.datetimeNullish().safeParse(undefined)
|
||||
expect(parsed.success).toBe(true)
|
||||
expect(parsed.data).toBeUndefined()
|
||||
})
|
||||
|
||||
it('fails to parse datetime with offset when offset option is false', () => {
|
||||
const parsed = zz
|
||||
.datetimeNullish({ offset: false })
|
||||
.safeParse('2024-01-01T12:00:00+00:00')
|
||||
expect(parsed.success).toBe(false)
|
||||
expect(parsed.error?.issues).toHaveLength(1)
|
||||
expect(parsed.error?.issues).toMatchObject([
|
||||
expect.objectContaining({
|
||||
code: 'invalid_format',
|
||||
format: 'datetime',
|
||||
message: 'Invalid ISO datetime',
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it('fails to parse invalid datetime strings', () => {
|
||||
const parsed = zz.datetimeNullish().safeParse('invalid-datetime')
|
||||
expect(parsed.success).toBe(false)
|
||||
expect(parsed.error?.issues).toHaveLength(1)
|
||||
expect(parsed.error?.issues).toMatchObject([
|
||||
expect.objectContaining({
|
||||
code: 'invalid_format',
|
||||
format: 'datetime',
|
||||
message: 'Invalid ISO datetime',
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
import z from 'zod'
|
||||
|
||||
export interface DatetimeSchemaOptions extends z.core.$ZodISODateTimeParams {
|
||||
allowNull?: boolean
|
||||
allowUndefined?: boolean
|
||||
}
|
||||
@@ -12,14 +12,14 @@ const { ParamsError } = require('./Errors')
|
||||
*/
|
||||
|
||||
/**
|
||||
* Parse and validate a request against a Zod schema
|
||||
* Validate a request against a zod schema
|
||||
*
|
||||
* @template {ZodType} T
|
||||
* @param {Request} req - The Express request object
|
||||
* @param {T} schema - The Zod schema to validate against
|
||||
* @returns {output<T>} The validated request object
|
||||
*/
|
||||
function parseReq(req, schema) {
|
||||
function validateReq(req, schema) {
|
||||
const parsed = schema.safeParse(req)
|
||||
if (parsed.success) {
|
||||
return parsed.data
|
||||
@@ -31,4 +31,4 @@ function parseReq(req, schema) {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { parseReq }
|
||||
module.exports = { validateReq }
|
||||
@@ -1,66 +0,0 @@
|
||||
// @ts-check
|
||||
const { isZodErrorLike } = require('zod-validation-error')
|
||||
|
||||
/**
|
||||
* @typedef {import('zod').ZodType} ZodType
|
||||
*/
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {import('zod').output<T>} output<T>
|
||||
*/
|
||||
|
||||
/**
|
||||
* A helper function to safely get a nested value from an object
|
||||
* using a path array (e.g., ["query", "resource_type"])
|
||||
* @param {any} data
|
||||
* @param {Array<PropertyKey>} path
|
||||
*/
|
||||
function getPathValue(data, path) {
|
||||
let current = data
|
||||
for (const key of path) {
|
||||
if (current === null || typeof current !== 'object') {
|
||||
return undefined
|
||||
}
|
||||
current = current[key]
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} issue
|
||||
* @param {any} value
|
||||
*/
|
||||
const isRequiredError = (issue, value) =>
|
||||
value === undefined &&
|
||||
(issue.code === 'invalid_type' || issue.code === 'invalid_union')
|
||||
|
||||
/**
|
||||
* Validates data against a Zod schema and throws a user-friendly error.
|
||||
*
|
||||
* @template {ZodType} T
|
||||
* @param {T} schema - The Zod schema
|
||||
* @param {unknown} data - The data to validate
|
||||
* @returns {output<T>} The validated (and transformed) data
|
||||
*/
|
||||
function validateSchema(schema, data) {
|
||||
try {
|
||||
return schema.parse(data)
|
||||
} catch (err) {
|
||||
if (isZodErrorLike(err)) {
|
||||
const errorMessages = err.issues.map(issue => {
|
||||
const value = getPathValue(data, issue.path)
|
||||
const fieldName = String(issue.path[issue.path.length - 1])
|
||||
if (isRequiredError(issue, value)) {
|
||||
return `"${fieldName}" is required`
|
||||
}
|
||||
return `"${fieldName}" - ` + issue.message
|
||||
})
|
||||
|
||||
throw new Error(errorMessages.join('; '))
|
||||
}
|
||||
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { validateSchema }
|
||||
@@ -3,23 +3,6 @@ const mongodb = require('mongodb')
|
||||
|
||||
const { ObjectId } = mongodb
|
||||
|
||||
/**
|
||||
* @import { DatetimeSchemaOptions } from './types'
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {DatetimeSchemaOptions} options
|
||||
*/
|
||||
const datetimeSchema = ({ allowNull, allowUndefined, ...zodOptions } = {}) => {
|
||||
const union = [z.date(), z.iso.datetime(zodOptions)]
|
||||
if (allowNull) union.push(z.null())
|
||||
if (allowUndefined) union.push(z.undefined())
|
||||
return z.union(union).transform(dt => {
|
||||
if (allowNull && !dt) return dt === null ? null : undefined
|
||||
return dt instanceof Date ? dt : new Date(dt)
|
||||
})
|
||||
}
|
||||
|
||||
const zz = {
|
||||
objectId: () =>
|
||||
z.string().refine(ObjectId.isValid, { message: 'invalid Mongo ObjectId' }),
|
||||
@@ -29,10 +12,6 @@ const zz = {
|
||||
.refine(ObjectId.isValid, { message: 'invalid Mongo ObjectId' })
|
||||
.transform(val => new ObjectId(val)),
|
||||
hex: () => z.string().regex(/^[0-9a-f]*$/),
|
||||
datetime: options => datetimeSchema(options),
|
||||
datetimeNullable: options => datetimeSchema({ ...options, allowNull: true }),
|
||||
datetimeNullish: options =>
|
||||
datetimeSchema({ ...options, allowNull: true, allowUndefined: true }),
|
||||
}
|
||||
|
||||
module.exports = { zz }
|
||||
|
||||
Generated
+4267
-11042
File diff suppressed because it is too large
Load Diff
+10
-12
@@ -8,9 +8,8 @@
|
||||
"@types/chai": "^4.3.0",
|
||||
"@types/chai-as-promised": "^7.1.8",
|
||||
"@types/mocha": "^10.0.6",
|
||||
"@typescript-eslint/eslint-plugin": "8.50.0",
|
||||
"@typescript-eslint/parser": "^8.50.0",
|
||||
"@vitest/eslint-plugin": "^1.5.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.30.1",
|
||||
"@typescript-eslint/parser": "^8.30.1",
|
||||
"eslint": "^8.15.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-config-standard": "^17.0.0",
|
||||
@@ -23,21 +22,22 @@
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-promise": "^6.0.0",
|
||||
"eslint-plugin-unicorn": "^56.0.0",
|
||||
"prettier": "3.7.4",
|
||||
"prettier": "3.6.2",
|
||||
"prettier-plugin-groovy": "0.2.1",
|
||||
"typescript": "^5.9.3"
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"engines": {
|
||||
"npm": "11.4.2"
|
||||
},
|
||||
"overrides": {
|
||||
"swagger-tools@0.10.4": {
|
||||
"path-to-regexp": "3.3.0",
|
||||
"body-parser": "1.20.3",
|
||||
"multer": "2.0.2"
|
||||
},
|
||||
"request@2.88.2": {
|
||||
"tough-cookie": "5.1.2",
|
||||
"form-data": "2.5.5",
|
||||
"qs": "6.14.1"
|
||||
},
|
||||
"streamdown": {
|
||||
"mermaid": "file:./services/web/stubs/mermaid-stub"
|
||||
"form-data": "2.5.5"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
@@ -67,7 +67,6 @@
|
||||
"services/history-v1",
|
||||
"services/idp",
|
||||
"services/latexqc",
|
||||
"services/linked-url-proxy",
|
||||
"services/notifications",
|
||||
"services/project-history",
|
||||
"services/real-time",
|
||||
@@ -77,7 +76,6 @@
|
||||
"services/third-party-references",
|
||||
"services/tpdsworker",
|
||||
"services/web",
|
||||
"tools/dependency-management",
|
||||
"tools/saas-e2e",
|
||||
"tools/migrations"
|
||||
]
|
||||
|
||||
+22
-27
@@ -1,35 +1,37 @@
|
||||
diff --git a/node_modules/@google-cloud/storage/node_modules/teeny-request/build/src/index.js b/node_modules/@google-cloud/storage/node_modules/teeny-request/build/src/index.js
|
||||
index af5d15e..8a9bf77 100644
|
||||
index a2251ca..e29e796 100644
|
||||
--- a/node_modules/@google-cloud/storage/node_modules/teeny-request/build/src/index.js
|
||||
+++ b/node_modules/@google-cloud/storage/node_modules/teeny-request/build/src/index.js
|
||||
@@ -168,31 +168,34 @@ function teenyRequest(reqOpts, callback) {
|
||||
@@ -166,27 +166,27 @@ function teenyRequest(reqOpts, callback) {
|
||||
}
|
||||
if (callback === undefined) {
|
||||
// Stream mode
|
||||
const requestStream = streamEvents(new stream_1.PassThrough());
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
- const requestStream = streamEvents(new stream_1.PassThrough());
|
||||
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
- let responseStream;
|
||||
- requestStream.once('reading', () => {
|
||||
- if (responseStream) {
|
||||
- (0, stream_1.pipeline)(responseStream, requestStream, () => { });
|
||||
- responseStream.pipe(requestStream);
|
||||
- }
|
||||
- else {
|
||||
- requestStream.once('response', () => {
|
||||
- (0, stream_1.pipeline)(responseStream, requestStream, () => { });
|
||||
- responseStream.pipe(requestStream);
|
||||
- });
|
||||
- }
|
||||
- });
|
||||
+ const requestStream = new stream_1.PassThrough();
|
||||
+ // // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
+ // let responseStream;
|
||||
+ // requestStream.once('reading', () => {
|
||||
+ // if (responseStream) {
|
||||
+ // (0, stream_1.pipeline)(responseStream, requestStream, () => { });
|
||||
+ // }
|
||||
+ // else {
|
||||
+ // requestStream.once('response', () => {
|
||||
+ // (0, stream_1.pipeline)(responseStream, requestStream, () => { });
|
||||
+ // });
|
||||
+ // }
|
||||
+ // if (responseStream) {
|
||||
+ // responseStream.pipe(requestStream);
|
||||
+ // }
|
||||
+ // else {
|
||||
+ // requestStream.once('response', () => {
|
||||
+ // responseStream.pipe(requestStream);
|
||||
+ // });
|
||||
+ // }
|
||||
+ // });
|
||||
+
|
||||
+
|
||||
options.compress = false;
|
||||
teenyRequest.stats.requestStarting();
|
||||
(0, node_fetch_1.default)(uri, options).then(res => {
|
||||
@@ -39,17 +41,10 @@ index af5d15e..8a9bf77 100644
|
||||
- requestStream.emit('error', err);
|
||||
- });
|
||||
+ teenyRequest.stats.requestFinished(); stream_1.pipeline(res.body, requestStream, () => {});
|
||||
+ // responseStream = res.body;
|
||||
+ // responseStream.on('error', (err) => {
|
||||
+ // requestStream.emit('error', err);
|
||||
+ // });
|
||||
+ // responseStream = res.body;
|
||||
+ // responseStream.on('error', (err) => {
|
||||
+ // requestStream.emit('error', err);
|
||||
+ // });
|
||||
const response = fetchToRequestResponse(options, res);
|
||||
requestStream.emit('response', response);
|
||||
}, err => {
|
||||
teenyRequest.stats.requestFinished();
|
||||
requestStream.emit('error', err);
|
||||
});
|
||||
+
|
||||
// fetch doesn't supply the raw HTTP stream, instead it
|
||||
// returns a PassThrough piped from the HTTP response
|
||||
// stream.
|
||||
@@ -21,9 +21,5 @@
|
||||
"overrides": [
|
||||
// Extra rules for Cypress tests
|
||||
{ "files": ["**/*.spec.ts"], "extends": ["plugin:cypress/recommended"] }
|
||||
],
|
||||
"ignorePatterns": [
|
||||
"hotfix/",
|
||||
"develop/"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ ENV TEXMFVAR=/var/lib/overleaf/tmp/texmf-var
|
||||
|
||||
# Update to ensure dependencies are updated
|
||||
# ------------------------------------------
|
||||
ENV REBUILT_AFTER="2026-01-13"
|
||||
ENV REBUILT_AFTER="2025-10-30"
|
||||
|
||||
# Install dependencies
|
||||
# --------------------
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
*/
|
||||
let redisConfig, siteUrl
|
||||
let e
|
||||
const Path = require('node:path')
|
||||
const Path = require('path')
|
||||
|
||||
// These credentials are used for authenticating api requests
|
||||
// between services that may need to go over public channels
|
||||
@@ -385,16 +385,7 @@ if (process.env.OVERLEAF_EMAIL_FROM_ADDRESS != null) {
|
||||
}
|
||||
|
||||
if (
|
||||
process.env.OVERLEAF_EMAIL_SMTP_USER != null &&
|
||||
process.env.OVERLEAF_EMAIL_SMTP_PASS_FILE != null
|
||||
) {
|
||||
settings.email.parameters.auth = {
|
||||
user: process.env.OVERLEAF_EMAIL_SMTP_USER,
|
||||
pass: require("fs").readFileSync(process.env.OVERLEAF_EMAIL_SMTP_PASS_FILE, "utf8")
|
||||
}
|
||||
}
|
||||
else if (
|
||||
process.env.OVERLEAF_EMAIL_SMTP_USER != null &&
|
||||
process.env.OVERLEAF_EMAIL_SMTP_USER != null ||
|
||||
process.env.OVERLEAF_EMAIL_SMTP_PASS != null
|
||||
) {
|
||||
settings.email.parameters.auth = {
|
||||
@@ -500,9 +491,9 @@ if (
|
||||
// With lots of incoming and outgoing HTTP connections to different services,
|
||||
// sometimes long running, it is a good idea to increase the default number
|
||||
// of sockets that Node will hold open.
|
||||
const http = require('node:http')
|
||||
const http = require('http')
|
||||
http.globalAgent.maxSockets = 300
|
||||
const https = require('node:https')
|
||||
const https = require('https')
|
||||
https.globalAgent.maxSockets = 300
|
||||
|
||||
module.exports = settings
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
FROM overleafcep/sharelatex:5.5.6-ext-v3.2
|
||||
|
||||
# add normalize and sanitize to linked-url-proxy
|
||||
COPY *.patch* .
|
||||
RUN bash -ec 'for p in *.patch; do echo "=== Applying $p ==="; patch -p1 < "$p" && rm $p; done' \
|
||||
&& npm install als-normalize-urlpath@2.3.0 \
|
||||
&& rm -f ./package.json.orig \
|
||||
./services/history-v1/storage/scripts/back_fill_file_hash.mjs.orig \
|
||||
./services/history-v1/test/acceptance/js/storage/back_fill_file_hash.test.mjs.orig \
|
||||
./services/linked-url-proxy/app/js/LinkedUrlProxyController.mjs.orig \
|
||||
./services/web/config/settings.defaults.js.orig \
|
||||
./services/web/frontend/js/features/source-editor/components/toolbar/toolbar-items.tsx.orig \
|
||||
./services/web/test/frontend/features/share-project-modal/components/share-project-modal.test.jsx.orig \
|
||||
./services/web/package.json.orig \
|
||||
./services/filestore/app.js.orig \
|
||||
./package-lock.json.orig \
|
||||
./package-lock.json.diff \
|
||||
/etc/my_init.d/100_make_overleaf_data_dirs.sh.orig \
|
||||
/etc/overleaf/settings.js.orig
|
||||
|
||||
@@ -1,256 +0,0 @@
|
||||
diff --git a/package.json b/package.json
|
||||
index 64fbd258ed..7cc3c7f9b3 100644
|
||||
--- a/package.json
|
||||
+++ b/package.json
|
||||
@@ -66,6 +66,7 @@
|
||||
"services/history-v1",
|
||||
"services/idp",
|
||||
"services/latexqc",
|
||||
+ "services/linked-url-proxy",
|
||||
"services/notifications",
|
||||
"services/project-history",
|
||||
"services/real-time",
|
||||
diff --git a/services/linked-url-proxy/Dockerfile b/services/linked-url-proxy/Dockerfile
|
||||
index 677006182c..7f2053f614 100644
|
||||
--- a/services/linked-url-proxy/Dockerfile
|
||||
+++ b/services/linked-url-proxy/Dockerfile
|
||||
@@ -2,7 +2,7 @@
|
||||
# Instead run bin/update_build_scripts from
|
||||
# https://github.com/overleaf/internal/
|
||||
|
||||
-FROM node:20.18.2 AS base
|
||||
+FROM node:22.18.0 AS base
|
||||
|
||||
WORKDIR /overleaf/services/linked-url-proxy
|
||||
|
||||
@@ -13,16 +13,20 @@ RUN mkdir /home/node/.config && chown node:node /home/node/.config
|
||||
FROM base AS app
|
||||
|
||||
COPY package.json package-lock.json /overleaf/
|
||||
+COPY libraries/fetch-utils/package.json /overleaf/libraries/fetch-utils/package.json
|
||||
COPY libraries/logger/package.json /overleaf/libraries/logger/package.json
|
||||
COPY libraries/metrics/package.json /overleaf/libraries/metrics/package.json
|
||||
+COPY libraries/o-error/package.json /overleaf/libraries/o-error/package.json
|
||||
COPY libraries/settings/package.json /overleaf/libraries/settings/package.json
|
||||
COPY services/linked-url-proxy/package.json /overleaf/services/linked-url-proxy/package.json
|
||||
COPY patches/ /overleaf/patches/
|
||||
|
||||
RUN cd /overleaf && npm ci --quiet
|
||||
|
||||
+COPY libraries/fetch-utils/ /overleaf/libraries/fetch-utils/
|
||||
COPY libraries/logger/ /overleaf/libraries/logger/
|
||||
COPY libraries/metrics/ /overleaf/libraries/metrics/
|
||||
+COPY libraries/o-error/ /overleaf/libraries/o-error/
|
||||
COPY libraries/settings/ /overleaf/libraries/settings/
|
||||
COPY services/linked-url-proxy/ /overleaf/services/linked-url-proxy/
|
||||
|
||||
diff --git a/services/linked-url-proxy/app/js/LinkedUrlProxyController.mjs b/services/linked-url-proxy/app/js/LinkedUrlProxyController.mjs
|
||||
index 1269eede0d..ea82f65d16 100644
|
||||
--- a/services/linked-url-proxy/app/js/LinkedUrlProxyController.mjs
|
||||
+++ b/services/linked-url-proxy/app/js/LinkedUrlProxyController.mjs
|
||||
@@ -1,4 +1,6 @@
|
||||
import dns from 'dns/promises'
|
||||
+import { sanitizeUrl } from 'strict-url-sanitise'
|
||||
+import normalizeUrlPath from 'als-normalize-urlpath'
|
||||
import ipaddr from 'ipaddr.js'
|
||||
import { URL } from 'node:url'
|
||||
import { Transform } from 'node:stream'
|
||||
@@ -18,15 +20,7 @@ function isBlockedIp(ipStr, targetUrl) {
|
||||
}
|
||||
|
||||
const range = addr.range()
|
||||
- if ([
|
||||
- 'loopback',
|
||||
- 'private',
|
||||
- 'linkLocal',
|
||||
- 'multicast',
|
||||
- 'reserved',
|
||||
- 'broadcast',
|
||||
- 'unspecified'
|
||||
- ].includes(range)) {
|
||||
+ if (!['unicast'].includes(addr.range())) {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -44,7 +38,7 @@ function isBlockedIp(ipStr, targetUrl) {
|
||||
return false
|
||||
}
|
||||
|
||||
-async function validateSourceUrl(hostname, targetUrl) {
|
||||
+async function checkUrlAccess(hostname, targetUrl) {
|
||||
const records = await dns.lookup(hostname, { all: true }).catch(() => [])
|
||||
if (!records.length) {
|
||||
const err = new Error(`DNS lookup failed for ${hostname}`)
|
||||
@@ -62,22 +56,40 @@ async function validateSourceUrl(hostname, targetUrl) {
|
||||
}
|
||||
}
|
||||
|
||||
-async function fetchValidated(urlStr, redirectCount = 0) {
|
||||
+async function validateAndFetch(rawUrl, redirectCount = 0) {
|
||||
if (redirectCount > Settings.maxRedirects) {
|
||||
const err = new Error('Too many redirects')
|
||||
err.info = { status: 421 }
|
||||
throw err
|
||||
}
|
||||
|
||||
- const url = new URL(urlStr)
|
||||
+ const sanitizedUrl = sanitizeUrl(rawUrl)
|
||||
+ if (!sanitizedUrl) {
|
||||
+ const err = new Error(`Invalid or unsafe URL: ${rawUrl}`)
|
||||
+ err.info = { status: 400 }
|
||||
+ throw err
|
||||
+ }
|
||||
+
|
||||
+ const url = new URL(sanitizedUrl)
|
||||
+
|
||||
if (!['http:', 'https:'].includes(url.protocol)) {
|
||||
const err = new Error(`${url.protocol} protocol is not allowed`)
|
||||
err.info = { status: 400 }
|
||||
throw err
|
||||
}
|
||||
|
||||
- // Validate DNS and blocked IPs
|
||||
- await validateSourceUrl(url.hostname, urlStr)
|
||||
+ const normalizedPath = normalizeUrlPath(url.pathname).pathname
|
||||
+
|
||||
+ if (!normalizedPath) {
|
||||
+ const err = new Error(`Invalid or unsafe URL path: ${url.pathname}`)
|
||||
+ err.info = { status: 400 }
|
||||
+ throw err
|
||||
+ }
|
||||
+
|
||||
+ const normalizedUrl = url.toString()
|
||||
+
|
||||
+ // check DNS and allowed resources
|
||||
+ await checkUrlAccess(url.hostname, normalizedUrl)
|
||||
|
||||
const opts = {
|
||||
redirect: 'manual',
|
||||
@@ -86,7 +98,7 @@ async function fetchValidated(urlStr, redirectCount = 0) {
|
||||
}
|
||||
|
||||
try {
|
||||
- const { stream, response } = await fetchStreamWithResponse(urlStr, opts)
|
||||
+ const { stream, response } = await fetchStreamWithResponse(normalizedUrl, opts)
|
||||
|
||||
const contentLengthHeader = response.headers.get('content-length')
|
||||
if (contentLengthHeader) {
|
||||
@@ -112,8 +124,8 @@ async function fetchValidated(urlStr, redirectCount = 0) {
|
||||
if (status >= 300 && status < 400) {
|
||||
const location = err.response.headers.get('Location')
|
||||
if (location) {
|
||||
- const nextUrl = new URL(location, url).toString()
|
||||
- return fetchValidated(nextUrl, redirectCount + 1)
|
||||
+ const nextUrl = new URL(location, normalizedUrl).toString()
|
||||
+ return validateAndFetch(nextUrl, redirectCount + 1)
|
||||
} else {
|
||||
const e = new Error('Redirect response missing Location header')
|
||||
e.info = { status: 421 }
|
||||
@@ -141,14 +153,14 @@ async function proxy(req, res) {
|
||||
return
|
||||
}
|
||||
|
||||
- const { stream: upstreamStream, response, headers } = await fetchValidated(targetUrl)
|
||||
+ const { stream: upstreamStream, response, headers } = await validateAndFetch(targetUrl)
|
||||
|
||||
res.statusCode = response.status || 200
|
||||
res.setHeader('Content-Type', headers['content-type'] || 'application/octet-stream')
|
||||
res.setHeader('Cache-Control', 'no-store')
|
||||
|
||||
function onError(err) {
|
||||
- logger.warn({ err, url: req.url }, 'linked-url-proxy request failed')
|
||||
+ logger.info({ err, url: req.url }, 'linked-url-proxy request failed')
|
||||
try { upstreamStream.destroy() } catch (_) {}
|
||||
if (!res.headersSent) {
|
||||
let body = `Error: ${err?.message ?? String(err)}`
|
||||
@@ -163,11 +175,9 @@ async function proxy(req, res) {
|
||||
upstreamStream.pipe(res)
|
||||
|
||||
} catch (err) {
|
||||
- logger.warn({ err, url: req.url }, 'linked-url-proxy request failed')
|
||||
-
|
||||
- let status = err.info.status
|
||||
+ const status = err.info?.status || 500
|
||||
+ logger.info({ linkedUrl: err.message, status, url: req.url }, 'linked-url-proxy request failed')
|
||||
let body = `Error: ${err.message || String(err)}`
|
||||
-
|
||||
try {
|
||||
res.writeHead(status, { 'Content-Type': 'text/plain' })
|
||||
res.end(body)
|
||||
diff --git a/services/linked-url-proxy/package.json b/services/linked-url-proxy/package.json
|
||||
index 845551c8ef..8081a80822 100644
|
||||
--- a/services/linked-url-proxy/package.json
|
||||
+++ b/services/linked-url-proxy/package.json
|
||||
@@ -2,18 +2,24 @@
|
||||
"name": "@overleaf/linked-url-proxy",
|
||||
"description": "An API for providing linked url proxy",
|
||||
"private": true,
|
||||
- "type": "module",
|
||||
"main": "app.mjs",
|
||||
"scripts": {
|
||||
- "start": "node app.mjs"
|
||||
+ "start": "node app.mjs",
|
||||
+ "nodemon": "node --watch app.mjs"
|
||||
},
|
||||
- "version": "0.1.0",
|
||||
+ "version": "0.1.1",
|
||||
"dependencies": {
|
||||
"@overleaf/settings": "*",
|
||||
"@overleaf/logger": "*",
|
||||
"@overleaf/metrics": "*",
|
||||
"async": "^3.2.5",
|
||||
- "express": "^4.21.2"
|
||||
- "ipaddr.js": "^1.9.1"
|
||||
+ "express": "^4.21.2",
|
||||
+ "ipaddr.js": "^1.9.1",
|
||||
+ "als-normalize-urlpath": "^2.3.0",
|
||||
+ "strict-url-sanitise": "^0.0.1"
|
||||
+ },
|
||||
+ "devDependencies": {
|
||||
+ "als-normalize-urlpath": "^2.3.0",
|
||||
+ "strict-url-sanitise": "^0.0.1"
|
||||
}
|
||||
}
|
||||
diff --git a/services/linked-url-proxy/tsconfig.json b/services/linked-url-proxy/tsconfig.json
|
||||
index c018d6e682..0639496cc8 100644
|
||||
--- a/services/linked-url-proxy/tsconfig.json
|
||||
+++ b/services/linked-url-proxy/tsconfig.json
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.backend.json",
|
||||
"include": [
|
||||
- "app.js",
|
||||
+ "app.mjs",
|
||||
"app.ts",
|
||||
"app/js/**/*",
|
||||
"benchmarks/**/*",
|
||||
diff --git a/services/web/package.json b/services/web/package.json
|
||||
index 21da1188a2..c2c4fcfaba 100644
|
||||
--- a/services/web/package.json
|
||||
+++ b/services/web/package.json
|
||||
@@ -69,6 +69,7 @@
|
||||
"last 1 year",
|
||||
"safari > 14"
|
||||
],
|
||||
+
|
||||
"dependencies": {
|
||||
"@contentful/rich-text-html-renderer": "^16.0.2",
|
||||
"@contentful/rich-text-types": "^16.0.2",
|
||||
@@ -92,6 +93,7 @@
|
||||
"@xmldom/xmldom": "^0.7.13",
|
||||
"accepts": "^1.3.7",
|
||||
"ajv": "^8.12.0",
|
||||
+ "als-normalize-urlpath": "^2.3.0",
|
||||
"archiver": "^5.3.0",
|
||||
"async": "^3.2.5",
|
||||
"base-x": "^4.0.1",
|
||||
@@ -166,7 +168,8 @@
|
||||
"request": "^2.88.2",
|
||||
"requestretry": "^7.1.0",
|
||||
"sanitize-html": "^2.8.1",
|
||||
- "stripe": "^18.1.0",
|
||||
+ "strict-url-sanitise": "^0.0.1",
|
||||
+ "stripe": "^18.4.0",
|
||||
"tough-cookie": "^4.0.0",
|
||||
"tsscmp": "^1.0.6",
|
||||
"uid-safe": "^2.1.5",
|
||||
@@ -1,6 +0,0 @@
|
||||
FROM sharelatex/sharelatex:5.5.6
|
||||
|
||||
# Apply security updates to base image
|
||||
RUN apt update && apt install -y linux-libc-dev \
|
||||
&& unattended-upgrade --verbose --no-minimal-upgrade-steps \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user