diff --git a/.componentsignore b/.componentsignore new file mode 100644 index 0000000..41b42e6 --- /dev/null +++ b/.componentsignore @@ -0,0 +1,3 @@ +[ + +] diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..bd6f481 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,24 @@ +name: lint + +on: + workflow_call: + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Use Node.js + uses: actions/setup-node@v3 + with: + node-version: 20.x + - name: Check out repository + uses: actions/checkout@v3 + - name: Load cache + uses: actions/cache@v2 + with: + path: '**/node_modules' + key: ${{ runner.os }}-lint-modules-${{ secrets.CACHE_VERSION }}-${{ hashFiles('**/yarn.lock') }} + - name: Install dependencies + run: yarn install --pure-lockfile + - name: Run linter + run: yarn run lint diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..4c8d67e --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,11 @@ +name: push +on: [push] + +jobs: + test: + # Run the tests in every case + uses: ./.github/workflows/test.yml + + lint: + # Run the tests in every case + uses: ./.github/workflows/lint.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..c970c7a --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,57 @@ +name: test + +on: + workflow_call: + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + node-version: + - 18.x + - 20.x + steps: + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + - name: Ensure line endings are consistent + run: git config --global core.autocrlf input + - name: Check out repository + uses: actions/checkout@v3 + - name: Load cache + uses: actions/cache@v2 + with: + path: | + **/node_modules + .rdf-test-suite-cache + .rdf-test-suite-ldf-cache + key: ${{ runner.os }}-test-modules-${{ secrets.CACHE_VERSION }}-${{ hashFiles('**/yarn.lock') }} + - name: Install dependencies + run: yarn install --pure-lockfile + - name: Build project + run: yarn run build + - name: Run depcheck + run: yarn run depcheck + - name: Run browser-tests + run: yarn run test-browser + - name: Run tests + run: yarn run test-ci + - name: Submit coverage results + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.github_token }} + flag-name: run-${{ matrix.node-version }} + parallel: true + + coveralls: + needs: test + runs-on: ubuntu-latest + steps: + - name: Consolidate test coverage from different jobs + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.github_token }} + parallel-finished: true diff --git a/config/default.json b/config/default.json new file mode 100644 index 0000000..e0b9339 --- /dev/null +++ b/config/default.json @@ -0,0 +1,9 @@ +{ + "@context": [ + "https://linkedsoftwaredependencies.org/bundles/npm/componentsjs/^5.0.0/components/context.jsonld", + "https://linkedsoftwaredependencies.org/bundles/npm/solid-aggregator/^0.0.0/components/context.jsonld" + ], + "@id": "urn:solid-aggregator:endpoint", + "@type": "Endpoint", + "test": "test" +} diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..acded86 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,26 @@ +const antfu = require('@antfu/eslint-config'); +const generalConfig = require('./eslint/general'); +const testConfig = require('./eslint/test'); +const typedConfig = require('./eslint/typed'); +const unicornConfig = require('./eslint/unicorn'); + +module.exports = antfu.default( + { + ignores: [ 'lib/**/*.js', 'lib/**/*.d.ts', '**/*.md' ], + }, + generalConfig, + unicornConfig, + typedConfig, + testConfig, + { + // JSON rules + files: [ '**/*.json' ], + rules: { + 'jsonc/array-bracket-spacing': [ 'error', 'always', { + singleValue: true, + objectsInArrays: false, + arraysInArrays: false, + }], + }, + }, +); diff --git a/eslint/general.js b/eslint/general.js new file mode 100644 index 0000000..bb01b86 --- /dev/null +++ b/eslint/general.js @@ -0,0 +1,114 @@ +module.exports = { + rules: { + 'antfu/consistent-list-newline': 'error', + + 'arrow-body-style': [ 'error', 'as-needed', { requireReturnForObjectLiteral: false }], + 'capitalized-comments': [ 'error', 'always', { ignoreConsecutiveComments: true }], + curly: [ 'error', 'all' ], + 'default-case': 'error', + eqeqeq: [ 'error', 'always' ], + 'for-direction': 'error', + 'func-style': [ 'error', 'declaration' ], + 'function-call-argument-newline': [ 'error', 'consistent' ], + 'function-paren-newline': [ 'error', 'consistent' ], + 'getter-return': [ 'error', { allowImplicit: true }], + 'grouped-accessor-pairs': [ 'error', 'getBeforeSet' ], + 'guard-for-in': 'error', + 'line-comment-position': [ 'error', { position: 'above' }], + 'linebreak-style': [ 'error', 'unix' ], + 'multiline-comment-style': [ 'error', 'separate-lines' ], + // Need to override `allow` value + 'no-console': [ 'error', { allow: [ '' ]}], + 'no-constructor-return': 'error', + 'no-dupe-else-if': 'error', + 'no-else-return': [ 'error', { allowElseIf: false }], + 'no-implicit-coercion': 'error', + 'no-implicit-globals': 'error', + 'no-lonely-if': 'error', + 'no-plusplus': [ 'error', { allowForLoopAfterthoughts: true }], + 'no-sync': [ 'error', { allowAtRootLevel: false }], + 'no-useless-concat': 'error', + 'no-useless-escape': 'error', + 'operator-assignment': [ 'error', 'always' ], + 'prefer-object-spread': 'error', + radix: 'error', + 'require-unicode-regexp': 'error', + 'require-yield': 'error', + 'sort-imports': [ + 'error', + { + allowSeparatedGroups: false, + ignoreCase: true, + ignoreDeclarationSort: true, + ignoreMemberSort: false, + memberSyntaxSortOrder: [ 'none', 'all', 'multiple', 'single' ], + }, + ], + + 'import/extensions': 'error', + + 'jsdoc/no-multi-asterisks': [ 'error', { allowWhitespace: true }], + + 'node/prefer-global/buffer': 'off', + 'node/prefer-global/process': 'off', + + 'style/array-bracket-spacing': [ 'error', 'always', { + singleValue: true, + objectsInArrays: false, + arraysInArrays: false, + }], + // Conflicts with style/object-curly-spacing + 'style/block-spacing': 'off', + 'style/brace-style': [ 'error', '1tbs', { allowSingleLine: false }], + 'style/generator-star-spacing': [ 'error', { before: false, after: true }], + // Seems to be inconsistent in when it adds indentation and when it does not + 'style/indent-binary-ops': 'off', + 'style/member-delimiter-style': [ 'error', { + multiline: { delimiter: 'semi', requireLast: true }, + singleline: { delimiter: 'semi', requireLast: false }, + }], + 'style/no-extra-parens': [ 'error', 'functions' ], + 'style/object-curly-spacing': [ 'error', 'always', { + objectsInObjects: false, + arraysInObjects: false, + }], + 'style/operator-linebreak': [ 'error', 'after' ], + 'style/semi': [ 'error', 'always' ], + 'style/semi-style': [ 'error', 'last' ], + 'style/space-before-function-paren': [ 'error', 'never' ], + 'style/switch-colon-spacing': 'error', + 'style/quote-props': [ 'error', 'as-needed', { + keywords: false, + unnecessary: true, + numbers: false, + }], + 'style/yield-star-spacing': [ 'error', 'after' ], + + 'ts/adjacent-overload-signatures': 'error', + 'ts/array-type': 'error', + 'ts/ban-ts-comment': [ 'error', { + 'ts-expect-error': true, + }], + 'ts/consistent-indexed-object-style': [ 'error', 'record' ], + 'ts/consistent-type-definitions': 'off', + 'ts/explicit-member-accessibility': 'error', + 'ts/method-signature-style': 'error', + 'ts/no-confusing-non-null-assertion': 'error', + 'ts/no-extraneous-class': [ 'error', { + allowConstructorOnly: false, + allowEmpty: false, + allowStaticOnly: false, + }], + 'ts/no-inferrable-types': [ 'error', { + ignoreParameters: false, + ignoreProperties: false, + }], + 'ts/prefer-for-of': 'error', + 'ts/prefer-function-type': 'error', + + 'unused-imports/no-unused-vars': [ + 'error', + { args: 'after-used', vars: 'all', ignoreRestSiblings: true }, + ], + }, +}; diff --git a/eslint/test.js b/eslint/test.js new file mode 100644 index 0000000..17f3cc1 --- /dev/null +++ b/eslint/test.js @@ -0,0 +1,53 @@ +const jest = require('eslint-plugin-jest'); + +// Specifically for tests +module.exports = { + // See https://github.com/jest-community/eslint-plugin-jest/issues/1408 + plugins: { + jest, + }, + files: [ '**/test/**/*.ts' ], + rules: { + ...jest.configs.all.rules, + // Rule is not smart enough to check called function in the test + 'jest/expect-expect': 'off', + 'jest/valid-title': [ 'error', { + mustNotMatch: { + describe: /\.$/u.source, + }, + mustMatch: { + it: /\.$/u.source, + }, + }], + + // Default rules that are overkill + 'jest/no-hooks': 'off', + 'jest/max-expects': 'off', + 'jest/no-conditional-in-test': 'off', + 'jest/prefer-expect-assertions': 'off', + 'jest/prefer-lowercase-title': 'off', + 'jest/prefer-strict-equal': 'off', + 'jest/require-hook': 'off', + + 'test/prefer-lowercase-title': 'off', + + 'ts/naming-convention': 'off', + 'ts/no-unsafe-argument': 'off', + 'ts/no-unsafe-assignment': 'off', + 'ts/no-unsafe-call': 'off', + 'ts/no-unsafe-member-access': 'off', + 'ts/no-unsafe-return': 'off', + 'ts/unbound-method': 'off', + + // Incorrectly detects usage of undefined in "toHaveBeenLastCalledWith" checks + 'unicorn/no-useless-undefined': 'off', + 'unicorn/filename-case': [ 'error', { + cases: { + camelCase: true, + pascalCase: false, + kebabCase: true, + snakeCase: false, + }, + }], + }, +}; diff --git a/eslint/typed.js b/eslint/typed.js new file mode 100644 index 0000000..b09e420 --- /dev/null +++ b/eslint/typed.js @@ -0,0 +1,96 @@ +// Copied from https://github.com/antfu/eslint-config/blob/main/src/configs/typescript.ts +// Doing it like this, so we can make sure these only try to trigger on *.ts files, +// Preventing issues with the *.js files. +const typeAwareRules = { + 'dot-notation': 'off', + 'no-implied-eval': 'off', + 'no-throw-literal': 'off', + 'ts/await-thenable': 'error', + 'ts/dot-notation': [ 'error', { allowKeywords: true }], + 'ts/no-floating-promises': 'error', + 'ts/no-for-in-array': 'error', + 'ts/no-implied-eval': 'error', + 'ts/no-misused-promises': 'error', + 'ts/no-throw-literal': 'error', + 'ts/no-unnecessary-type-assertion': 'error', + 'ts/no-unsafe-argument': 'error', + 'ts/no-unsafe-assignment': 'error', + 'ts/no-unsafe-call': 'error', + 'ts/no-unsafe-member-access': 'error', + 'ts/no-unsafe-return': 'error', + 'ts/restrict-plus-operands': 'error', + 'ts/restrict-template-expressions': 'error', + 'ts/unbound-method': 'error', +}; + +module.exports = { + // By default, antfu also triggers type rules on *.js files which causes all kinds of issues for us + files: [ '**/*.ts' ], + languageOptions: { + parserOptions: { + tsconfigRootDir: process.cwd(), + project: [ './lib/tsconfig-eslint.json' ], + }, + }, + rules: { + ...typeAwareRules, + 'ts/consistent-type-assertions': [ 'error', { + assertionStyle: 'as', + }], + 'ts/naming-convention': [ + 'error', + { + selector: 'default', + format: [ 'camelCase' ], + leadingUnderscore: 'forbid', + trailingUnderscore: 'forbid', + }, + { + selector: 'import', + format: null, + }, + { + selector: 'variable', + format: [ 'camelCase', 'UPPER_CASE' ], + leadingUnderscore: 'forbid', + trailingUnderscore: 'forbid', + }, + { + selector: 'typeLike', + format: [ 'PascalCase' ], + }, + { + selector: [ 'typeParameter' ], + format: [ 'PascalCase' ], + prefix: [ 'T' ], + }, + ], + 'ts/explicit-function-return-type': [ 'error', { + allowExpressions: false, + allowTypedFunctionExpressions: false, + allowHigherOrderFunctions: false, + }], + 'ts/no-base-to-string': 'error', + 'ts/no-floating-promises': [ 'error', { ignoreVoid: false }], + 'ts/promise-function-async': 'error', + 'ts/no-unnecessary-boolean-literal-compare': 'error', + 'ts/no-unnecessary-qualifier': 'error', + 'ts/prefer-nullish-coalescing': 'error', + 'ts/prefer-readonly': 'error', + 'ts/prefer-reduce-type-parameter': 'error', + 'ts/prefer-regexp-exec': 'error', + 'ts/prefer-string-starts-ends-with': 'error', + 'ts/require-array-sort-compare': 'error', + + // These are not type specific, but we only care about these in TS files + 'max-len': [ 'error', { code: 120, ignoreUrls: true }], + 'unicorn/filename-case': [ 'error', { + cases: { + camelCase: true, + pascalCase: true, + kebabCase: false, + snakeCase: false, + }, + }], + }, +}; diff --git a/eslint/unicorn.js b/eslint/unicorn.js new file mode 100644 index 0000000..337464d --- /dev/null +++ b/eslint/unicorn.js @@ -0,0 +1,60 @@ +module.exports = { + rules: { + 'unicorn/better-regex': 'error', + 'unicorn/empty-brace-spaces': 'error', + 'unicorn/consistent-function-scoping': 'error', + 'unicorn/expiring-todo-comments': [ 'error', { + ignoreDatesOnPullRequests: false, + terms: [ 'todo' ], + allowWarningComments: false, + }], + 'unicorn/explicit-length-check': 'error', + 'unicorn/filename-case': [ 'error', { + cases: { + camelCase: false, + pascalCase: false, + kebabCase: true, + snakeCase: false, + }, + }], + 'unicorn/new-for-builtins': 'error', + 'unicorn/no-array-for-each': 'error', + 'unicorn/no-array-reduce': 'error', + 'unicorn/no-for-loop': 'error', + 'unicorn/no-invalid-remove-event-listener': 'error', + 'unicorn/no-lonely-if': 'error', + 'unicorn/no-negated-condition': 'error', + 'unicorn/no-nested-ternary': 'error', + 'unicorn/no-object-as-default-parameter': 'error', + 'unicorn/no-process-exit': 'error', + 'unicorn/no-thenable': 'error', + 'unicorn/no-useless-fallback-in-spread': 'error', + 'unicorn/no-useless-length-check': 'error', + 'unicorn/no-useless-promise-resolve-reject': 'error', + 'unicorn/no-useless-spread': 'error', + 'unicorn/no-useless-undefined': 'error', + 'unicorn/no-zero-fractions': 'error', + 'unicorn/prefer-array-find': 'error', + 'unicorn/prefer-array-flat-map': 'error', + 'unicorn/prefer-array-index-of': 'error', + 'unicorn/prefer-array-some': 'error', + 'unicorn/prefer-at': 'error', + 'unicorn/prefer-code-point': 'error', + 'unicorn/prefer-date-now': 'error', + 'unicorn/prefer-default-parameters': 'error', + 'unicorn/prefer-math-trunc': 'error', + 'unicorn/prefer-native-coercion-functions': 'error', + 'unicorn/prefer-negative-index': 'error', + 'unicorn/prefer-object-from-entries': 'error', + 'unicorn/prefer-optional-catch-binding': 'error', + 'unicorn/prefer-reflect-apply': 'error', + 'unicorn/prefer-regexp-test': 'error', + 'unicorn/prefer-set-has': 'error', + 'unicorn/prefer-set-size': 'error', + 'unicorn/prefer-spread': 'error', + 'unicorn/prefer-string-replace-all': 'error', + 'unicorn/prefer-string-slice': 'error', + 'unicorn/require-array-join-separator': 'error', + 'unicorn/require-number-to-fixed-digits-argument': 'error', + }, +}; diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..a1bb419 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,50 @@ +const os = require('node:os'); +const v8 = require('node:v8'); + +// Several parts inspired by https://github.com/renovatebot/renovate/blob/main/package.json + +const ci = Boolean(process.env.CI); + +const cpus = os.cpus(); +const mem = os.totalmem(); +const stats = v8.getHeapStatistics(); + +if (ci) { + process.stderr.write(`Host stats: + Cpus: ${cpus.length} + Memory: ${(mem / 1024 / 1024 / 1024).toFixed(2)} GB + HeapLimit: ${(stats.heap_size_limit / 1024 / 1024 / 1024).toFixed(2)} GB +`); +} + +module.exports = { + transform: { + '^.+\\.ts$': [ 'ts-jest', { + // Enabling this can fix issues when using prereleases of typings packages + // isolatedModules: true + }], + }, + testRegex: [ '/test/.*-test.*.ts$' ], + moduleFileExtensions: [ + 'ts', + 'js', + ], + testEnvironment: 'node', + setupFilesAfterEnv: [ 'jest-rdf' ], + collectCoverage: true, + coveragePathIgnorePatterns: [ + '/node_modules/', + '/mocks/', + 'index.js', + '/engines/query-sparql/test/util.ts', + '/test/util/', + ], + coverageThreshold: { + global: { + branches: 100, + functions: 100, + lines: 100, + statements: 100, + }, + }, +};