feat(sdk): let docker:start choose the server version (#21690)
## What Makes `yarn twenty docker:start` version-selectable. Same core feature as #21686 — but here scaffolded apps default to `latest` (pinning is **opt-in**) rather than being pinned to the scaffolder's version. > Alternative to #21686. Pick one; the difference is only the scaffolded default. Two layers of resolution: 1. **Explicit flag** — `yarn twenty docker:start [version]`, mirroring the existing `docker:upgrade [version]`. 2. **App-pinned default** — when no version is passed, `docker:start` reads `twenty.serverVersion` from the app's `package.json`, falling back to `latest`. Generated apps ship `twenty.serverVersion: "latest"`, so default behavior is unchanged. To make the local server reproducible as code, set a version: ```json filename="package.json" { "twenty": { "serverVersion": "2.2.0" } } ``` ## Changes - `twenty-sdk`: new `getAppServerVersion()` util reads `twenty.serverVersion` from the cwd's `package.json`; `serverStart` gains a `version` option and resolves `option → app pin → latest`, building the image via `getImageForVersion()`; `docker:start [version]` (and the deprecated `server start [version]` alias) wired up. - `create-twenty-app`: template `package.json` ships `twenty.serverVersion: "latest"`. (`create-app` and the scaffolder are otherwise untouched.) - Docs: `local-server.mdx` documents version selection and the opt-in pin. ## Behavior notes - Default with no pin and no flag is `latest` — same as today. - Version only matters when **creating** a fresh container — an existing container keeps its image until `docker:upgrade` / `docker:reset`. ## Testing - New unit tests for `getAppServerVersion` (5 cases). - Extended the `app-template` scaffolding test to assert the `latest` default. - `twenty-sdk` cli vitest suite (273) and `create-twenty-app` jest suite (9) pass; oxlint + oxfmt clean on changed files. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21690?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
@@ -11,6 +11,7 @@ Use `yarn twenty docker:*` to control the local Twenty container:
|
||||
| Command | What it does |
|
||||
|---------|--------------|
|
||||
| `yarn twenty docker:start` | Start the server (pulls the image if needed) |
|
||||
| `yarn twenty docker:start 2.2.0` | Start a specific server version |
|
||||
| `yarn twenty docker:start --port 3030` | Start on a custom port |
|
||||
| `yarn twenty docker:stop` | Stop the server (preserves data) |
|
||||
| `yarn twenty docker:status` | Show URL, version, and login credentials |
|
||||
@@ -21,6 +22,20 @@ Use `yarn twenty docker:*` to control the local Twenty container:
|
||||
|
||||
Data persists across restarts in two Docker volumes (`twenty-app-dev-data` for PostgreSQL, `twenty-app-dev-storage` for files). Use `reset` to wipe everything.
|
||||
|
||||
## Pinning the server version
|
||||
|
||||
When no version is passed, `docker:start` resolves the version from your app's `engines.twenty` range in `package.json` — the same range the server validates against when your app is installed. It starts the newest published `twenty-app-dev` image that satisfies the range, falling back to `latest` when the field is absent or no published version matches:
|
||||
|
||||
```json filename="package.json"
|
||||
{
|
||||
"engines": {
|
||||
"twenty": ">=2.2.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Pass a version explicitly to override the range for a single run: `yarn twenty docker:start 2.3.0`. If a container already exists on a different version, `docker:start` upgrades it in place (recreating the container while preserving your data volumes).
|
||||
|
||||
## Upgrading the server image
|
||||
|
||||
`yarn twenty docker:upgrade` pulls the latest image, compares digests, and only recreates the container if anything actually changed. Volumes are preserved — only the container is replaced. If a new image was pulled and the container was running, the upgrade automatically starts a new container; run `yarn twenty docker:start` afterward to wait for it to become healthy.
|
||||
|
||||
@@ -85,6 +85,7 @@
|
||||
"preact": "^10.28.3",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"semver": "7.6.3",
|
||||
"tinyglobby": "^0.2.15",
|
||||
"twenty-client-sdk": "workspace:*",
|
||||
"typescript": "^5.9.3",
|
||||
@@ -95,6 +96,7 @@
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/react": "^19.2.0",
|
||||
"@types/react-dom": "^19.2.0",
|
||||
"@types/semver": "^7.5.8",
|
||||
"@typescript/native-preview": "^7.0.0-dev.20260116.1",
|
||||
"@vitest/coverage-v8": "^4.0.18",
|
||||
"prettier": "^3.1.1",
|
||||
|
||||
@@ -15,7 +15,10 @@ import chalk from 'chalk';
|
||||
import type { Command } from 'commander';
|
||||
import { execSync, spawnSync } from 'node:child_process';
|
||||
|
||||
const startAction = async (options: { port?: string; test?: boolean }) => {
|
||||
const startAction = async (
|
||||
version: string | undefined,
|
||||
options: { port?: string; test?: boolean },
|
||||
) => {
|
||||
const defaultPort = options.test ? DEFAULT_TEST_PORT : DEFAULT_PORT;
|
||||
const port = options.port ? parseInt(options.port, 10) : defaultPort;
|
||||
|
||||
@@ -27,6 +30,7 @@ const startAction = async (options: { port?: string; test?: boolean }) => {
|
||||
const result = await serverStart({
|
||||
port,
|
||||
test: options.test,
|
||||
version,
|
||||
onProgress: (message) => console.log(chalk.gray(message)),
|
||||
});
|
||||
|
||||
@@ -173,8 +177,10 @@ const upgradeAction = async (
|
||||
|
||||
export const registerServerCommands = (program: Command): void => {
|
||||
program
|
||||
.command('docker:start')
|
||||
.description('Start the local Twenty container')
|
||||
.command('docker:start [version]')
|
||||
.description(
|
||||
'Start the local Twenty container (version defaults to the app `engines.twenty` range, then `latest`)',
|
||||
)
|
||||
.option('-p, --port <port>', 'HTTP port')
|
||||
.option('--test', 'Start a separate test instance (port 2021)')
|
||||
.action(startAction);
|
||||
@@ -225,13 +231,18 @@ export const registerServerCommands = (program: Command): void => {
|
||||
);
|
||||
|
||||
server
|
||||
.command('start')
|
||||
.command('start [version]')
|
||||
.option('-p, --port <port>', 'HTTP port')
|
||||
.option('--test', 'Start a separate test instance (port 2021)')
|
||||
.action(async (options: { port?: string; test?: boolean }) => {
|
||||
deprecate('start', 'docker:start');
|
||||
await startAction(options);
|
||||
});
|
||||
.action(
|
||||
async (
|
||||
version: string | undefined,
|
||||
options: { port?: string; test?: boolean },
|
||||
) => {
|
||||
deprecate('start', 'docker:start');
|
||||
await startAction(version, options);
|
||||
},
|
||||
);
|
||||
|
||||
server
|
||||
.command('stop')
|
||||
|
||||
@@ -40,6 +40,8 @@ export {
|
||||
getImageDigest,
|
||||
getImageForVersion,
|
||||
} from '@/cli/utilities/server/docker-container';
|
||||
export { getEngineVersionRange } from '@/cli/utilities/version/get-engine-version-range';
|
||||
export { resolveHighestEngineVersion } from '@/cli/utilities/version/resolve-highest-engine-version';
|
||||
|
||||
// Config
|
||||
export { ConfigService } from '@/cli/utilities/config/config-service';
|
||||
|
||||
@@ -8,9 +8,10 @@ import {
|
||||
containerExists,
|
||||
DEFAULT_PORT,
|
||||
DEFAULT_TEST_PORT,
|
||||
getContainerImageTag,
|
||||
getContainerPort,
|
||||
getDockerNotRunningMessage,
|
||||
IMAGE,
|
||||
getImageForVersion,
|
||||
isContainerRunning,
|
||||
TEST_CONTAINER_NAME,
|
||||
} from '@/cli/utilities/server/docker-container';
|
||||
@@ -18,7 +19,9 @@ import {
|
||||
checkServerHealth,
|
||||
detectLocalServer,
|
||||
} from '@/cli/utilities/server/detect-local-server';
|
||||
import { serverUpgrade } from '@/cli/operations/server-upgrade';
|
||||
import { checkServerVersionCompatibility } from '@/cli/utilities/version/check-server-version-compatibility';
|
||||
import { resolveHighestEngineVersion } from '@/cli/utilities/version/resolve-highest-engine-version';
|
||||
import { execSync, spawn, spawnSync } from 'node:child_process';
|
||||
import chalk from 'chalk';
|
||||
|
||||
@@ -107,6 +110,7 @@ const waitForHealthy = async (
|
||||
export type ServerStartOptions = {
|
||||
port?: number;
|
||||
test?: boolean;
|
||||
version?: string;
|
||||
onProgress?: (message: string) => void;
|
||||
};
|
||||
|
||||
@@ -120,6 +124,9 @@ const innerServerStart = async (
|
||||
): Promise<CommandResult<ServerStartResult>> => {
|
||||
const { onProgress, test: isTest } = options;
|
||||
|
||||
const version = await resolveHighestEngineVersion(options.version);
|
||||
const image = getImageForVersion(version);
|
||||
|
||||
const containerName = isTest ? TEST_CONTAINER_NAME : CONTAINER_NAME;
|
||||
const defaultPort = isTest ? DEFAULT_TEST_PORT : DEFAULT_PORT;
|
||||
const volumeData = isTest
|
||||
@@ -129,6 +136,26 @@ const innerServerStart = async (
|
||||
? 'twenty-app-dev-test-storage'
|
||||
: 'twenty-app-dev-storage';
|
||||
|
||||
if (checkDockerRunning() && containerExists(containerName)) {
|
||||
const currentImage = getContainerImageTag(containerName);
|
||||
|
||||
if (currentImage !== null && currentImage !== image) {
|
||||
onProgress?.(
|
||||
`Existing container runs ${currentImage}; upgrading to ${image}...`,
|
||||
);
|
||||
|
||||
const upgradeResult = await serverUpgrade({
|
||||
version,
|
||||
test: isTest,
|
||||
onProgress,
|
||||
});
|
||||
|
||||
if (!upgradeResult.success) {
|
||||
return { success: false, error: upgradeResult.error };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const existingUrl = await detectLocalServer(options.port ?? defaultPort);
|
||||
|
||||
if (existingUrl) {
|
||||
@@ -150,9 +177,13 @@ const innerServerStart = async (
|
||||
}
|
||||
|
||||
if (!checkDockerRunning()) {
|
||||
const retryCommand = isTest
|
||||
? 'yarn twenty docker:start --test'
|
||||
: 'yarn twenty docker:start';
|
||||
const retryCommand = [
|
||||
'yarn twenty docker:start',
|
||||
version !== 'latest' ? version : null,
|
||||
isTest ? '--test' : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
|
||||
return {
|
||||
success: false,
|
||||
@@ -211,7 +242,9 @@ const innerServerStart = async (
|
||||
onProgress?.('Starting existing container...');
|
||||
execSync(`docker start ${containerName}`, { stdio: 'ignore' });
|
||||
} else {
|
||||
onProgress?.('Pulling Docker image and starting Twenty container...');
|
||||
onProgress?.(
|
||||
`Pulling Docker image (${image}) and starting Twenty container...`,
|
||||
);
|
||||
|
||||
const runResult = spawnSync(
|
||||
'docker',
|
||||
@@ -230,7 +263,7 @@ const innerServerStart = async (
|
||||
`${volumeData}:/data/postgres`,
|
||||
'-v',
|
||||
`${volumeStorage}:/app/packages/twenty-server/.local-storage`,
|
||||
IMAGE,
|
||||
image,
|
||||
],
|
||||
{ stdio: 'inherit' },
|
||||
);
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { getImageForVersion } from '@/cli/utilities/server/docker-container';
|
||||
|
||||
describe('getImageForVersion', () => {
|
||||
it('defaults to the latest tag', () => {
|
||||
expect(getImageForVersion()).toBe('twentycrm/twenty-app-dev:latest');
|
||||
});
|
||||
|
||||
it('keeps the latest tag as-is', () => {
|
||||
expect(getImageForVersion('latest')).toBe(
|
||||
'twentycrm/twenty-app-dev:latest',
|
||||
);
|
||||
});
|
||||
|
||||
it('prefixes a bare semver version with v to match published tags', () => {
|
||||
expect(getImageForVersion('2.7.5')).toBe('twentycrm/twenty-app-dev:v2.7.5');
|
||||
});
|
||||
|
||||
it('does not double-prefix an already v-prefixed version', () => {
|
||||
expect(getImageForVersion('v2.7.5')).toBe(
|
||||
'twentycrm/twenty-app-dev:v2.7.5',
|
||||
);
|
||||
});
|
||||
|
||||
it('prefixes a prerelease semver version', () => {
|
||||
expect(getImageForVersion('2.7.5-rc.1')).toBe(
|
||||
'twentycrm/twenty-app-dev:v2.7.5-rc.1',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -56,8 +56,11 @@ export const containerExists = (containerName = CONTAINER_NAME): boolean => {
|
||||
}
|
||||
};
|
||||
|
||||
export const getImageForVersion = (version = 'latest'): string =>
|
||||
`twentycrm/twenty-app-dev:${version}`;
|
||||
export const getImageForVersion = (version = 'latest'): string => {
|
||||
const tag = /^\d+\.\d+\.\d+/.test(version) ? `v${version}` : version;
|
||||
|
||||
return `twentycrm/twenty-app-dev:${tag}`;
|
||||
};
|
||||
|
||||
export const getContainerDigest = (
|
||||
containerName = CONTAINER_NAME,
|
||||
@@ -76,6 +79,23 @@ export const getContainerDigest = (
|
||||
}
|
||||
};
|
||||
|
||||
export const getContainerImageTag = (
|
||||
containerName = CONTAINER_NAME,
|
||||
): string | null => {
|
||||
try {
|
||||
return execFileSync(
|
||||
'docker',
|
||||
['inspect', '-f', '{{.Config.Image}}', containerName],
|
||||
{
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'ignore'],
|
||||
},
|
||||
).trim();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const getImageDigest = (image: string): string | null => {
|
||||
try {
|
||||
return execFileSync('docker', ['inspect', '-f', '{{.Id}}', image], {
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
|
||||
import { getEngineVersionRange } from '@/cli/utilities/version/get-engine-version-range';
|
||||
|
||||
describe('getEngineVersionRange', () => {
|
||||
const createdDirs: string[] = [];
|
||||
|
||||
const seed = (contents: unknown): string => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'twenty-app-server-range-'));
|
||||
|
||||
writeFileSync(join(dir, 'package.json'), JSON.stringify(contents), 'utf-8');
|
||||
createdDirs.push(dir);
|
||||
|
||||
return dir;
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
while (createdDirs.length > 0) {
|
||||
rmSync(createdDirs.pop() as string, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('returns the engines.twenty range', () => {
|
||||
const dir = seed({ engines: { twenty: '>=2.2.0' } });
|
||||
|
||||
expect(getEngineVersionRange(dir)).toBe('>=2.2.0');
|
||||
});
|
||||
|
||||
it('trims surrounding whitespace', () => {
|
||||
const dir = seed({ engines: { twenty: ' ^2.2.0 ' } });
|
||||
|
||||
expect(getEngineVersionRange(dir)).toBe('^2.2.0');
|
||||
});
|
||||
|
||||
it('returns null when engines.twenty is missing', () => {
|
||||
const dir = seed({ engines: { node: '^24.0.0' } });
|
||||
|
||||
expect(getEngineVersionRange(dir)).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null when engines.twenty is empty', () => {
|
||||
const dir = seed({ engines: { twenty: ' ' } });
|
||||
|
||||
expect(getEngineVersionRange(dir)).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null when engines.twenty is not a string', () => {
|
||||
const dir = seed({ engines: { twenty: 2 } });
|
||||
|
||||
expect(getEngineVersionRange(dir)).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null when no package.json exists', () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'twenty-app-server-range-empty-'));
|
||||
|
||||
createdDirs.push(dir);
|
||||
|
||||
expect(getEngineVersionRange(dir)).toBeNull();
|
||||
});
|
||||
});
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { getEngineVersionRange } from '@/cli/utilities/version/get-engine-version-range';
|
||||
import { getPublishedServerVersions } from '@/cli/utilities/version/get-published-server-versions';
|
||||
import { resolveHighestEngineVersion } from '@/cli/utilities/version/resolve-highest-engine-version';
|
||||
|
||||
vi.mock('@/cli/utilities/version/get-engine-version-range');
|
||||
vi.mock('@/cli/utilities/version/get-published-server-versions');
|
||||
|
||||
const mockedGetRange = vi.mocked(getEngineVersionRange);
|
||||
const mockedGetPublished = vi.mocked(getPublishedServerVersions);
|
||||
|
||||
const publish = (...names: string[]) =>
|
||||
mockedGetPublished.mockResolvedValue(
|
||||
names.map((name) => ({ name, lastUpdatedAt: new Date(0) })),
|
||||
);
|
||||
|
||||
describe('resolveHighestEngineVersion', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockedGetRange.mockReturnValue(null);
|
||||
mockedGetPublished.mockResolvedValue([]);
|
||||
});
|
||||
|
||||
it('returns the explicit version verbatim without resolving', async () => {
|
||||
expect(await resolveHighestEngineVersion('2.3.1')).toBe('2.3.1');
|
||||
expect(mockedGetRange).not.toHaveBeenCalled();
|
||||
expect(mockedGetPublished).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('trims the explicit version', async () => {
|
||||
expect(await resolveHighestEngineVersion(' 2.3.1 ')).toBe('2.3.1');
|
||||
});
|
||||
|
||||
it('falls through to the app range when the explicit version is blank', async () => {
|
||||
mockedGetRange.mockReturnValue('>=2.2.0');
|
||||
publish('2.2.0', '2.3.0');
|
||||
|
||||
expect(await resolveHighestEngineVersion(' ')).toBe('2.3.0');
|
||||
});
|
||||
|
||||
it('returns latest when the app declares no range', async () => {
|
||||
mockedGetRange.mockReturnValue(null);
|
||||
|
||||
expect(await resolveHighestEngineVersion()).toBe('latest');
|
||||
expect(mockedGetPublished).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns latest when the range is not a valid semver range', async () => {
|
||||
mockedGetRange.mockReturnValue('not-a-range');
|
||||
|
||||
expect(await resolveHighestEngineVersion()).toBe('latest');
|
||||
expect(mockedGetPublished).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns the highest published version satisfying the range', async () => {
|
||||
mockedGetRange.mockReturnValue('^2.2.0');
|
||||
publish('2.1.9', '2.2.0', '2.2.5', '2.3.0', '3.0.0', 'latest');
|
||||
|
||||
expect(await resolveHighestEngineVersion()).toBe('2.3.0');
|
||||
});
|
||||
|
||||
it('returns latest when no published version satisfies the range', async () => {
|
||||
mockedGetRange.mockReturnValue('>=9.0.0');
|
||||
publish('2.2.0', '2.3.0');
|
||||
|
||||
expect(await resolveHighestEngineVersion()).toBe('latest');
|
||||
});
|
||||
|
||||
it('returns latest when no versions are published', async () => {
|
||||
mockedGetRange.mockReturnValue('>=2.2.0');
|
||||
mockedGetPublished.mockResolvedValue([]);
|
||||
|
||||
expect(await resolveHighestEngineVersion()).toBe('latest');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
|
||||
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
|
||||
|
||||
export const getEngineVersionRange = (
|
||||
cwd: string = CURRENT_EXECUTION_DIRECTORY,
|
||||
): string | null => {
|
||||
try {
|
||||
const pkg = JSON.parse(
|
||||
readFileSync(join(cwd, 'package.json'), 'utf-8'),
|
||||
) as { engines?: { twenty?: unknown } };
|
||||
|
||||
const range = pkg.engines?.twenty;
|
||||
|
||||
return typeof range === 'string' && range.trim() !== ''
|
||||
? range.trim()
|
||||
: null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import semver from 'semver';
|
||||
|
||||
import { getEngineVersionRange } from '@/cli/utilities/version/get-engine-version-range';
|
||||
import { getPublishedServerVersions } from '@/cli/utilities/version/get-published-server-versions';
|
||||
|
||||
export const resolveHighestEngineVersion = async (
|
||||
explicitVersion?: string,
|
||||
): Promise<string> => {
|
||||
const explicit = explicitVersion?.trim();
|
||||
|
||||
if (explicit) {
|
||||
return explicit;
|
||||
}
|
||||
|
||||
const range = getEngineVersionRange();
|
||||
|
||||
if (range === null || semver.validRange(range) === null) {
|
||||
return 'latest';
|
||||
}
|
||||
|
||||
const published = await getPublishedServerVersions();
|
||||
|
||||
const bestMatch = published
|
||||
.map((version) => version.name)
|
||||
.filter((name) => semver.valid(name) !== null)
|
||||
.filter((name) => semver.satisfies(name, range))
|
||||
.sort(semver.rcompare)[0];
|
||||
|
||||
return bestMatch ?? 'latest';
|
||||
};
|
||||
@@ -23192,6 +23192,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/semver@npm:^7.5.8":
|
||||
version: 7.7.1
|
||||
resolution: "@types/semver@npm:7.7.1"
|
||||
checksum: 10c0/c938aef3bf79a73f0f3f6037c16e2e759ff40c54122ddf0b2583703393d8d3127130823facb880e694caa324eb6845628186aac1997ee8b31dc2d18fafe26268
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/send@npm:*":
|
||||
version: 0.17.4
|
||||
resolution: "@types/send@npm:0.17.4"
|
||||
@@ -53617,6 +53624,7 @@ __metadata:
|
||||
"@types/node": "npm:^24.0.0"
|
||||
"@types/react": "npm:^19.2.0"
|
||||
"@types/react-dom": "npm:^19.2.0"
|
||||
"@types/semver": "npm:^7.5.8"
|
||||
"@typescript/native-preview": "npm:^7.0.0-dev.20260116.1"
|
||||
"@vitest/coverage-v8": "npm:^4.0.18"
|
||||
axios: "npm:^1.16.0"
|
||||
@@ -53634,6 +53642,7 @@ __metadata:
|
||||
react: "npm:^19.2.0"
|
||||
react-dom: "npm:^19.2.0"
|
||||
rollup-plugin-dts: "npm:^6.4.1"
|
||||
semver: "npm:7.6.3"
|
||||
tinyglobby: "npm:^0.2.15"
|
||||
ts-morph: "npm:^25.0.0"
|
||||
tsc-alias: "npm:^1.8.16"
|
||||
|
||||
Reference in New Issue
Block a user