Fix missing dependencies between packages (#121)

Why?  Few reasons:

- tsup treats dependencies as external code and does not bundle them
- without dependencies turborepo will always serve stale code when some of dependencies changed

Moving internal dependencies to devDependencies makes tsup treat them as non-external and turborepo still keep tracks of relations
This commit is contained in:
Kamil Kisiela 2022-05-27 14:15:11 +02:00 committed by GitHub
parent 40263a552a
commit 86f5945aab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 323 additions and 43 deletions

View file

@ -6,6 +6,7 @@ module.exports = {
reportUnusedDisableDirectives: true,
ignorePatterns: [
'scripts',
'rules',
'out',
'public',
'packages/web/app/src/graphql/index.ts',
@ -17,7 +18,7 @@ module.exports = {
sourceType: 'module',
},
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint', 'import'],
plugins: ['@typescript-eslint', 'import', 'hive'],
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
rules: {
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', ignoreRestSiblings: true }],
@ -36,11 +37,17 @@ module.exports = {
'import/no-extraneous-dependencies': [
'error',
{
devDependencies: ['packages/services/storage/tools/*.js'],
devDependencies: ['packages/services/storage/tools/*.js', 'packages/services/**'],
optionalDependencies: false,
},
],
'no-restricted-imports': ['error', { patterns: ['packages/*'] }],
'hive/enforce-deps-in-dev': [
'error',
{
scopes: ['@hive', '@graphql-hive'],
ignored: ['packages/libraries/**', 'packages/web/**'],
},
],
// 🚨 The following rules needs to be fixed and was temporarily disabled to avoid printing warning
'@typescript-eslint/no-explicit-any': 'off',

View file

@ -35,6 +35,7 @@
"turbo": "env-cmd --silent turbo run"
},
"devDependencies": {
"eslint-plugin-hive": "file:./rules",
"@changesets/cli": "2.22.0",
"@graphql-codegen/add": "3.1.1",
"@graphql-codegen/cli": "2.6.2",

View file

@ -2,7 +2,7 @@
"name": "@hive/api",
"type": "module",
"private": true,
"version": "0.0.2",
"version": "0.0.0",
"license": "MIT",
"peerDependencies": {
"graphql": "^16.0.0",
@ -45,6 +45,9 @@
"zod": "3.15.1"
},
"devDependencies": {
"@hive/schema": "0.0.0",
"@hive/tokens": "0.0.0",
"@graphql-hive/core": "0.2.2",
"@types/auth0": "2.34.2",
"@types/ioredis": "4.28.7",
"@types/lodash": "4.14.182",

View file

@ -16,6 +16,7 @@
"graphql": "16.5.0"
},
"devDependencies": {
"@hive/service-common": "0.0.0",
"fastify": "3.29.0",
"esbuild": "0.14.39",
"cross-undici-fetch": "0.1.27",

View file

@ -1,4 +1,3 @@
/* eslint-disable import/no-extraneous-dependencies */
import { Response, Request, Headers, ReadableStream } from 'cross-undici-fetch';
globalThis.Response = Response;

View file

@ -3,7 +3,7 @@
"type": "module",
"name": "@hive/rate-limit",
"description": "A microservice for Hive SaaS, that exposes information about rate limits per given org/target.",
"version": "0.0.1",
"version": "0.0.0",
"license": "MIT",
"scripts": {
"dev": "tsup-node src/dev.ts --watch --format esm --target node16 --onSuccess 'node dist/dev.js' | pino-pretty --translateTime HH:MM:ss TT --ignore pid,hostname",
@ -23,6 +23,8 @@
"got": "12.0.4"
},
"devDependencies": {
"@hive/service-common": "0.0.0",
"@hive/storage": "0.0.0",
"pino-pretty": "6.0.0"
},
"buildOptions": {

View file

@ -2,7 +2,7 @@
"name": "@hive/schema",
"private": true,
"type": "module",
"version": "0.2.1",
"version": "0.0.0",
"license": "MIT",
"scripts": {
"dev": "tsup-node src/dev.ts --format esm --target node16 --watch --onSuccess 'node dist/dev.js' | pino-pretty --translateTime HH:MM:ss TT --ignore pid,hostname",
@ -22,6 +22,7 @@
"ioredis": "4.28.3"
},
"devDependencies": {
"@hive/service-common": "0.0.0",
"@types/ioredis": "4.28.7",
"pino-pretty": "6.0.0"
},

View file

@ -3,7 +3,7 @@
"type": "module",
"private": true,
"bin": "index.js",
"version": "0.27.8",
"version": "0.0.0",
"license": "MIT",
"scripts": {
"dev": "tsup-node src/dev.ts --format esm --target node16 --watch --onSuccess 'node dist/dev.js' | pino-pretty --translateTime HH:MM:ss TT --ignore pid,hostname",
@ -25,6 +25,10 @@
"reflect-metadata": "0.1.13"
},
"devDependencies": {
"@graphql-hive/client": "0.15.4",
"@hive/api": "0.0.0",
"@hive/service-common": "0.0.0",
"@hive/storage": "0.0.0",
"pino-pretty": "6.0.0",
"@swc/core": "1.2.185"
},

View file

@ -2,7 +2,7 @@
"private": true,
"type": "module",
"name": "@hive/service-common",
"version": "0.1.3",
"version": "0.0.0",
"license": "MIT",
"dependencies": {
"fastify": "3.29.0",

View file

@ -56,28 +56,31 @@ services:
- 'stack'
zookeeper:
image: confluentinc/cp-zookeeper:6.2.2
image: confluentinc/cp-zookeeper:6.2.2-3-ubi8
hostname: zookeeper
networks:
- 'stack'
ports:
- '2181:2181'
environment:
BUMP: 1
ZOOKEEPER_CLIENT_PORT: 2181
ZOOKEEPER_TICK_TIME: 2000
ulimits:
nofile:
soft: 20000
hard: 40000
healthcheck:
test: ['CMD', 'cub', 'zk-ready', '127.0.0.1:2181', '10']
interval: 30s
interval: 5s
timeout: 10s
retries: 10
start_period: 30s
retries: 6
start_period: 15s
environment:
ZOOKEEPER_CLIENT_PORT: 2181
ZOOKEEPER_TICK_TIME: 2000
volumes:
- ./volumes/zookeeper/db:/var/lib/zookeeper/data
- ./volumes/zookeeper/log:/var/lib/zookeeper/log
broker:
image: confluentinc/cp-kafka:6.2.2
image: confluentinc/cp-kafka:6.2.2-3-ubi8
hostname: borker
depends_on:
zookeeper:
@ -87,19 +90,20 @@ services:
ports:
- '29092:29092'
- '9092:9092'
healthcheck:
test: ['CMD', 'cub', 'kafka-ready', '1', '5', '-b', '127.0.0.1:9092', '-c', '/etc/kafka/kafka.properties']
interval: 30s
timeout: 10s
retries: 10
start_period: 30s
ulimits:
nofile:
soft: 16384
hard: 16384
soft: 20000
hard: 40000
healthcheck:
test: ['CMD', 'cub', 'kafka-ready', '1', '5', '-b', '127.0.0.1:9092', '-c', '/etc/kafka/kafka.properties']
interval: 15s
timeout: 10s
retries: 6
start_period: 15s
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181'
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_AUTO_CREATE_TOPICS_ENABLE: 'true'
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://broker:29092,PLAINTEXT_HOST://localhost:9092
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1

View file

@ -2,7 +2,7 @@
"name": "@hive/storage",
"type": "module",
"private": true,
"version": "0.14.1",
"version": "0.0.0",
"license": "MIT",
"scripts": {
"setup": "yarn db:start && yarn db",

View file

@ -13,8 +13,10 @@ import type {
Alert,
AuthProvider,
OrganizationBilling,
Storage,
ProjectType,
OrganizationType,
} from '@hive/api';
import { Storage, ProjectType, OrganizationType } from '@hive/api';
import { sql, TaggedTemplateLiteralInvocationType } from 'slonik';
import {
commits,
@ -97,7 +99,7 @@ export async function createStorage(connection: string): Promise<Storage> {
schemaPush: parseInt(organization.limit_schema_push_monthly),
},
billingPlan: organization.plan_name,
type: organization.type === 'PERSONAL' ? OrganizationType.PERSONAL : OrganizationType.REGULAR,
type: (organization.type === 'PERSONAL' ? 'PERSONAL' : 'REGULAR') as OrganizationType,
};
}
@ -570,7 +572,7 @@ export async function createStorage(connection: string): Promise<Storage> {
},
async getMyOrganization({ user }) {
const org = await pool.maybeOne<Slonik<organizations>>(
sql`SELECT * FROM public.organizations WHERE user_id = ${user} AND type = ${OrganizationType.PERSONAL} LIMIT 1`
sql`SELECT * FROM public.organizations WHERE user_id = ${user} AND type = ${'PERSONAL'} LIMIT 1`
);
return org ? transformOrganization(org) : null;

View file

@ -3,7 +3,7 @@
"type": "module",
"name": "@hive/stripe-billing",
"description": "A microservice for Hive SaaS, that syncs usage information to Stripe (metered billing)",
"version": "0.0.1",
"version": "0.0.0",
"license": "MIT",
"scripts": {
"dev": "tsup-node src/dev.ts --format esm --target node16 --watch --onSuccess 'node dist/dev.js' | pino-pretty --translateTime HH:MM:ss TT --ignore pid,hostname",
@ -24,6 +24,8 @@
"got": "12.0.4"
},
"devDependencies": {
"@hive/service-common": "0.0.0",
"@hive/storage": "0.0.0",
"pino-pretty": "6.0.0"
},
"buildOptions": {

View file

@ -2,7 +2,7 @@
"name": "@hive/tokens",
"type": "module",
"private": true,
"version": "0.6.8",
"version": "0.0.0",
"license": "MIT",
"scripts": {
"dev": "tsup-node src/dev.ts --format esm --target node16 --watch --onSuccess 'node dist/dev.js' | pino-pretty --translateTime HH:MM:ss TT --ignore pid,hostname",
@ -21,6 +21,8 @@
"reflect-metadata": "0.1.13"
},
"devDependencies": {
"@hive/service-common": "0.0.0",
"@hive/storage": "0.0.0",
"@types/ms": "0.7.31",
"pino-pretty": "6.0.0"
},

View file

@ -1,7 +1,7 @@
{
"name": "@hive/usage-common",
"private": true,
"version": "0.0.1",
"version": "0.0.0",
"license": "MIT",
"dependencies": {
"graphql": "16.5.0"

View file

@ -3,7 +3,7 @@
"type": "module",
"name": "@hive/usage-estimator",
"description": "A microservice for Hive SaaS, that calculates and exposes usage information.",
"version": "0.0.1",
"version": "0.0.0",
"license": "MIT",
"scripts": {
"dev": "tsup-node src/dev.ts --format esm --target node16 --watch --onSuccess 'node dist/dev.js' | pino-pretty --translateTime HH:MM:ss TT --ignore pid,hostname",
@ -21,6 +21,9 @@
"got": "12.0.4"
},
"devDependencies": {
"@hive/api": "0.0.0",
"@hive/service-common": "0.0.0",
"@hive/storage": "0.0.0",
"pino-pretty": "6.0.0"
},
"buildOptions": {

View file

@ -2,7 +2,7 @@
"private": true,
"type": "module",
"name": "@hive/usage-ingestor",
"version": "0.0.2",
"version": "0.0.0",
"license": "MIT",
"scripts": {
"dev": "tsup-node src/dev.ts --watch --format esm --target node16 --onSuccess 'node dist/dev.js' | pino-pretty --translateTime HH:MM:ss TT --ignore pid,hostname",
@ -22,6 +22,9 @@
"tiny-lru": "8.0.2"
},
"devDependencies": {
"@graphql-hive/core": "0.2.2",
"@hive/service-common": "0.0.0",
"@hive/usage-common": "0.0.0",
"pino-pretty": "6.0.0"
},
"buildOptions": {

View file

@ -2,7 +2,7 @@
"name": "@hive/usage",
"type": "module",
"private": true,
"version": "0.18.0",
"version": "0.0.0",
"license": "MIT",
"scripts": {
"dev": "tsup-node src/dev.ts --format esm --target node16 --watch --onSuccess 'node dist/dev.js' | pino-pretty --translateTime HH:MM:ss TT --ignore pid,hostname",
@ -21,6 +21,9 @@
"tiny-lru": "8.0.2"
},
"devDependencies": {
"@hive/usage-common": "0.0.0",
"@hive/service-common": "0.0.0",
"@hive/tokens": "0.0.0",
"pino-pretty": "6.0.0"
},
"buildOptions": {

View file

@ -2,7 +2,7 @@
"name": "@hive/webhooks",
"type": "module",
"private": true,
"version": "0.1.4",
"version": "0.0.0",
"license": "MIT",
"scripts": {
"dev": "tsup-node src/dev.ts --format esm --target node16 --watch --onSuccess 'node dist/dev.js' | pino-pretty --translateTime HH:MM:ss TT --ignore pid,hostname",
@ -20,6 +20,7 @@
"p-timeout": "5.0.2"
},
"devDependencies": {
"@hive/service-common": "0.0.0",
"ioredis": "4.28.3",
"copyfiles": "2.4.1",
"pino-pretty": "6.0.0"

View file

@ -1,7 +1,7 @@
{
"name": "@hive/app",
"private": true,
"version": "0.16.2",
"version": "0.0.0",
"scripts": {
"dev": "next dev",
"start": "next start",

View file

@ -6,6 +6,7 @@ import { useQuery } from 'urql';
import { useUser } from '@/components/auth/AuthProvider';
import { Avatar, Button, DropdownMenu, HiveLink } from '@/components/v2';
import {
AlertTriangleIcon,
ArrowDownIcon,
CalendarIcon,
FileTextIcon,
@ -15,7 +16,6 @@ import {
PlusIcon,
SettingsIcon,
TrendingUpIcon,
AlertTriangleIcon,
} from '@/components/v2/icon';
import { CreateOrganizationModal } from '@/components/v2/modals';
import { MeDocument, OrganizationsDocument, OrganizationsQuery, OrganizationType } from '@/graphql';

View file

@ -1,5 +1,5 @@
import { ForwardRefExoticComponent } from 'react';
import { Root, Item } from '@radix-ui/react-toggle-group';
import { Item, Root } from '@radix-ui/react-toggle-group';
import clsx from 'clsx';
type PropsOf<T> = T extends ForwardRefExoticComponent<infer P> ? P : unknown;

View file

@ -1,7 +1,7 @@
{
"name": "@hive/docs",
"private": true,
"version": "0.0.1",
"version": "0.0.0",
"scripts": {
"dev": "next dev",
"start": "next start",

View file

@ -1,7 +1,7 @@
{
"name": "@hive/landing-page",
"private": true,
"version": "0.1.20",
"version": "0.0.0",
"scripts": {
"dev": "next dev",
"build": "bob runify --single"

View file

@ -0,0 +1,227 @@
/**
* Why? Few reasons:
* - tsup treats dependencies as external code and does not bundle them
* - without dependencies turborepo will always serve stale code when some of dependencies changed
*
* Moving internal dependencies to devDependencies makes tsup treat them as non-external and turborepo still keep tracks of relations
*/
/// @ts-check
const path = require('path');
const fs = require('fs');
const minimatch = require('minimatch');
const readPkgUp = require('eslint-module-utils/readPkgUp').default;
const moduleVisitor = require('eslint-module-utils/moduleVisitor').default;
function isHivePackage(packageName, scopes) {
return typeof packageName === 'string' && scopes.some(scope => packageName.startsWith(`${scope}/`));
}
const depFieldCache = new Map();
function hasKeys(obj = {}) {
return Object.keys(obj).length > 0;
}
function extractDepFields(pkg) {
return {
name: pkg.name,
dependencies: pkg.dependencies || {},
devDependencies: pkg.devDependencies || {},
optionalDependencies: pkg.optionalDependencies || {},
peerDependencies: pkg.peerDependencies || {},
};
}
function getDependencies(context, packageDir) {
let paths = [];
try {
const packageContent = {
name: '',
dependencies: {},
devDependencies: {},
optionalDependencies: {},
peerDependencies: {},
};
if (packageDir && packageDir.length > 0) {
if (!Array.isArray(packageDir)) {
paths = [path.resolve(packageDir)];
} else {
paths = packageDir.map(dir => path.resolve(dir));
}
}
if (paths.length > 0) {
// use rule config to find package.json
paths.forEach(dir => {
const packageJsonPath = path.join(dir, 'package.json');
if (!depFieldCache.has(packageJsonPath)) {
const depFields = extractDepFields(JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')));
depFieldCache.set(packageJsonPath, depFields);
}
const _packageContent = depFieldCache.get(packageJsonPath);
Object.keys(packageContent).forEach(depsKey =>
Object.assign(packageContent[depsKey], _packageContent[depsKey])
);
});
} else {
// use closest package.json
Object.assign(
packageContent,
extractDepFields(
readPkgUp({
cwd: context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename(),
normalize: false,
}).pkg
)
);
}
if (
![
packageContent.dependencies,
packageContent.devDependencies,
packageContent.optionalDependencies,
packageContent.peerDependencies,
].some(hasKeys)
) {
return null;
}
return packageContent;
} catch (e) {
if (paths.length > 0 && e.code === 'ENOENT') {
context.report({
message: 'The package.json file could not be found.',
loc: { line: 0, column: 0 },
});
}
if (e.name === 'JSONError' || e instanceof SyntaxError) {
context.report({
message: 'The package.json file could not be parsed: ' + e.message,
loc: { line: 0, column: 0 },
});
}
return null;
}
}
function missingErrorMessage(packageName) {
return `'${packageName}' should be listed in the project's devDependencies. `;
}
function directDepErrorMessage(packageName) {
return `'${packageName}' should be listed in the project's devDependencies, not dependencies.`;
}
function optDepErrorMessage(packageName) {
return `'${packageName}' should be listed in the project's devDependencies, not optionalDependencies`;
}
function peerDepErrorMessage(packageName) {
return `'${packageName}' should be listed in the project's devDependencies, not peerDependencies`;
}
function getModuleOriginalName(name) {
const [first, second] = name.split('/');
return first.startsWith('@') ? `${first}/${second}` : first;
}
function checkDependencyDeclaration(deps, packageName, declarationStatus) {
const newDeclarationStatus = declarationStatus || {
isInDeps: false,
isInDevDeps: false,
isInOptDeps: false,
isInPeerDeps: false,
};
// in case of sub package.json inside a module
// check the dependencies on all hierarchy
const packageHierarchy = [];
const packageNameParts = packageName ? packageName.split('/') : [];
packageNameParts.forEach((namePart, index) => {
if (!namePart.startsWith('@')) {
const ancestor = packageNameParts.slice(0, index + 1).join('/');
packageHierarchy.push(ancestor);
}
});
return packageHierarchy.reduce((result, ancestorName) => {
return {
isInDeps: result.isInDeps || deps.dependencies[ancestorName] !== undefined,
isInDevDeps: result.isInDevDeps || deps.devDependencies[ancestorName] !== undefined,
isInOptDeps: result.isInOptDeps || deps.optionalDependencies[ancestorName] !== undefined,
isInPeerDeps: result.isInPeerDeps || deps.peerDependencies[ancestorName] !== undefined,
};
}, newDeclarationStatus);
}
function reportIfMissing(context, deps, node, name, scopes) {
if (node.importKind === 'type' || node.importKind === 'typeof') {
return;
}
if (!isHivePackage(name, scopes)) {
return;
}
const importPackageName = getModuleOriginalName(name);
let declarationStatus = checkDependencyDeclaration(deps, importPackageName);
if (declarationStatus.isInDevDeps) {
return;
}
if (declarationStatus.isInDeps) {
context.report(node, directDepErrorMessage(importPackageName));
return;
}
if (declarationStatus.isInOptDeps) {
context.report(node, optDepErrorMessage(importPackageName));
return;
}
if (declarationStatus.isInPeerDeps) {
context.report(node, peerDepErrorMessage(importPackageName));
return;
}
context.report(node, missingErrorMessage(importPackageName));
}
module.exports = {
meta: {
type: 'problem',
},
create(context) {
const options = context.options[0] || {};
const deps = getDependencies(context, options.packageDir) || extractDepFields({});
if (Array.isArray(options.ignored)) {
const filepath = context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename();
if (
options.ignored.some(
ignored => minimatch(filepath, ignored) || minimatch(filepath, path.join(process.cwd(), ignored))
)
) {
return {};
}
}
if (!Array.isArray(options.scopes)) {
throw new Error('[hive/enforce-deps-in-dev] The scopes option must be an array.');
}
return moduleVisitor(
(source, node) => {
reportIfMissing(context, deps, node, source.value, options.scopes);
},
{ commonjs: true }
);
},
};

5
rules/index.cjs Normal file
View file

@ -0,0 +1,5 @@
module.exports = {
rules: {
'enforce-deps-in-dev': require('./enforce-deps-in-dev.cjs'),
},
};

7
rules/package.json Normal file
View file

@ -0,0 +1,7 @@
{
"private": true,
"name": "eslint-plugin-hive",
"main": "index.cjs",
"type": "commonjs",
"version": "0.0.0"
}

View file

@ -8236,6 +8236,9 @@ eslint-module-utils@^2.7.3:
debug "^3.2.7"
find-up "^2.1.0"
"eslint-plugin-hive@file:./rules":
version "0.0.0"
eslint-plugin-import@2.26.0:
version "2.26.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz#f812dc47be4f2b72b478a021605a59fc6fe8b88b"