WIP: [web] shared template contracts and preparation #121
No files matched your search
@@ -6,6 +6,10 @@ on:
|
||||
- main
|
||||
paths:
|
||||
- "packages/web/templates/**"
|
||||
- "packages/web/pieces/**"
|
||||
- "packages/web/db/**"
|
||||
- "packages/web/config/**"
|
||||
- "apps/core/**"
|
||||
- "pnpm-workspace.yaml"
|
||||
- "pnpm-lock.yaml"
|
||||
- ".github/workflows/web-templates.yml"
|
||||
@@ -14,6 +18,10 @@ on:
|
||||
- main
|
||||
paths:
|
||||
- "packages/web/templates/**"
|
||||
- "packages/web/pieces/**"
|
||||
- "packages/web/db/**"
|
||||
- "packages/web/config/**"
|
||||
- "apps/core/**"
|
||||
- "pnpm-workspace.yaml"
|
||||
- "pnpm-lock.yaml"
|
||||
- ".github/workflows/web-templates.yml"
|
||||
@@ -37,6 +45,9 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build contract type dependencies
|
||||
run: pnpm --filter @luzzle/core --filter @luzzle/web.config --filter @luzzle/web.db --filter @luzzle/web.pieces build
|
||||
|
||||
- name: Build runtime
|
||||
run: pnpm --filter @luzzle/web.templates build
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ validation.
|
||||
After installing workspace dependencies, from the repository root:
|
||||
|
||||
```sh
|
||||
pnpm --filter @luzzle/core --filter @luzzle/web.config --filter @luzzle/web.db --filter @luzzle/web.pieces build
|
||||
pnpm --filter @luzzle/web.templates build
|
||||
pnpm --filter @luzzle/web.templates lint
|
||||
pnpm --filter @luzzle/web.templates test
|
||||
@@ -35,6 +36,38 @@ Build output is cleaned before each release build. Tests and reusable helpers
|
||||
create artifacts only in caller-selected directories; package tests use
|
||||
`node_modules/.cache/` and clean their scratch trees. Nothing uses `/tmp`.
|
||||
|
||||
## Shared role contracts
|
||||
|
||||
These internal exports contain types only, not executable modules:
|
||||
|
||||
| Export | Types |
|
||||
| -------------------- | ------------------------------------------------------------ |
|
||||
| `./pieces/props` | Piece page, icon, Open Graph props; icon component inputs |
|
||||
| `./site/props` | Root, feed, and error page props |
|
||||
| `./components/props` | Navigation/component inputs, resolved sizes, page components |
|
||||
| `./helpers` | Bound piece helpers and the common helper interface |
|
||||
|
||||
They keep `data`, `options`, `helpers`, and approved `components` separate. Piece
|
||||
records retain the canonical `PublicWebPiece` shape, including supplied assets
|
||||
and metadata. Type names are arbitrary strings, not a built-in allowlist. A type
|
||||
parameter can narrow that string without changing the record's public shape.
|
||||
These are shared consumer contracts, not a standalone author SDK or generated
|
||||
schema-derived editor declarations.
|
||||
|
||||
Component-call icon inputs permit omitted height/lazy/active values. Authored
|
||||
icon options require resolved height and explicit defaults. Pages have normalized
|
||||
tags; icons/Open Graph props do not expose page components or unrelated props.
|
||||
The low-level renderer below is not a role-specific input producer or validator.
|
||||
|
||||
Declaration emission runs after Rollup, which cleans `dist`. Emission uses
|
||||
TypeScript's isolated declarations and `--noCheck` so the lean runtime-only Docker
|
||||
proof does not need workspace declaration builds. This does **not** replace
|
||||
semantic verification: `check` and `test` run mandatory checked compilation,
|
||||
including compile-only positive/negative contract fixtures. Build the workspace
|
||||
type dependencies shown above before running them. Emitted declarations reference
|
||||
`@luzzle/web.pieces` and Svelte types; both are available in the internal developer
|
||||
workspace, not dependencies of the isolated runtime installation.
|
||||
|
||||
## Minimal runtime API
|
||||
|
||||
```js
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import js from '@eslint/js';
|
||||
import prettier from 'eslint-config-prettier';
|
||||
import svelte from 'eslint-plugin-svelte';
|
||||
import ts from 'typescript-eslint';
|
||||
import globals from 'globals';
|
||||
|
||||
export default [
|
||||
{ ignores: ['dist/**', 'node_modules/**'] },
|
||||
js.configs.recommended,
|
||||
...svelte.configs['flat/recommended'],
|
||||
...ts.configs.recommended.map((config) => ({ ...config, files: ['**/*.ts'] })),
|
||||
prettier,
|
||||
...svelte.configs['flat/prettier'],
|
||||
{ languageOptions: { globals: globals.node } }
|
||||
|
||||
@@ -13,10 +13,11 @@
|
||||
"README.md"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "rollup --config",
|
||||
"build": "rollup --config && tsc -p tsconfig.types.json --noCheck",
|
||||
"check": "tsc -p tsconfig.check.json",
|
||||
"lint": "eslint . && prettier --check .",
|
||||
"format": "prettier --write .",
|
||||
"test": "node --conditions=node --conditions=production --test test/*.test.js"
|
||||
"test": "tsc -p tsconfig.check.json && node --conditions=node --conditions=production --test test/*.test.js"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
@@ -32,6 +33,18 @@
|
||||
"default": "./dist/compatibility.js"
|
||||
},
|
||||
"./compatibility.json": "./dist/compatibility.json",
|
||||
"./pieces/props": {
|
||||
"types": "./dist/pieces/props.d.ts"
|
||||
},
|
||||
"./site/props": {
|
||||
"types": "./dist/site/props.d.ts"
|
||||
},
|
||||
"./components/props": {
|
||||
"types": "./dist/components/props.d.ts"
|
||||
},
|
||||
"./helpers": {
|
||||
"types": "./dist/helpers.d.ts"
|
||||
},
|
||||
"./runtime/svelte": {
|
||||
"development": "./dist/runtime/svelte.js",
|
||||
"default": "./dist/runtime/svelte.js"
|
||||
@@ -47,6 +60,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "catalog:",
|
||||
"@luzzle/web.pieces": "workspace:*",
|
||||
"@rollup/plugin-node-resolve": "16.0.3",
|
||||
"acorn": "8.16.0",
|
||||
"eslint": "catalog:",
|
||||
@@ -57,6 +71,8 @@
|
||||
"prettier": "catalog:",
|
||||
"prettier-plugin-svelte": "3.3.2",
|
||||
"rollup": "4.60.4",
|
||||
"svelte": "5.55.9"
|
||||
"svelte": "5.55.9",
|
||||
"typescript": "catalog:",
|
||||
"typescript-eslint": "catalog:"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { PublicWebPiece } from '@luzzle/web.pieces';
|
||||
import type { Component } from 'svelte';
|
||||
|
||||
/** Resolved positive pixel dimensions supplied to an authored template. */
|
||||
export interface TemplateSize {
|
||||
readonly width: number;
|
||||
readonly height: number;
|
||||
}
|
||||
|
||||
export interface NavBannerInputs {
|
||||
readonly background?: string;
|
||||
readonly color?: string;
|
||||
readonly hoverColor?: string;
|
||||
readonly showHome?: boolean;
|
||||
readonly showSearch?: boolean;
|
||||
readonly showThemeToggle?: boolean;
|
||||
readonly showProgress?: boolean;
|
||||
readonly showRandom?: boolean;
|
||||
}
|
||||
|
||||
/** Inputs at a page's component call, not the icon template's props. */
|
||||
export interface PieceIconInputs<T extends string = string> {
|
||||
readonly piece: PublicWebPiece & { readonly type: T };
|
||||
readonly size: {
|
||||
readonly width: number;
|
||||
/** Omission resolves to width * 1.5 before rendering the icon. */
|
||||
readonly height?: number;
|
||||
};
|
||||
readonly lazy?: boolean;
|
||||
readonly active?: boolean;
|
||||
}
|
||||
|
||||
export interface PageComponents {
|
||||
readonly NavBanner: Component<NavBannerInputs>;
|
||||
readonly PieceIcon: Component<PieceIconInputs>;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { PieceIconPalette, PublicWebPiece } from '@luzzle/web.pieces';
|
||||
|
||||
export type ImageFormat = 'jpg' | 'avif' | 'webp' | 'png';
|
||||
|
||||
export interface BoundPieceHelpers {
|
||||
getUrl(): string;
|
||||
getPalette(): PieceIconPalette | undefined;
|
||||
getImageUrl(
|
||||
assetKey: string | null | undefined,
|
||||
minWidth: number,
|
||||
format: ImageFormat
|
||||
): string | undefined;
|
||||
getAssetUrl(assetKey: string, transformation: string): string | undefined;
|
||||
getAssetContent(assetKey: string, transformation: string): string | undefined;
|
||||
}
|
||||
|
||||
export interface TemplateHelpers {
|
||||
forPiece(piece: PublicWebPiece): BoundPieceHelpers;
|
||||
getStaticAssetUrl(relativePath: string): string | undefined;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { PublicWebPiece } from '@luzzle/web.pieces';
|
||||
import type { PageComponents, TemplateSize } from '../components/props.js';
|
||||
import type { TemplateHelpers } from '../helpers.js';
|
||||
|
||||
export type { PieceIconInputs } from '../components/props.js';
|
||||
|
||||
export interface PieceTag {
|
||||
readonly tag: string;
|
||||
readonly slug: string;
|
||||
}
|
||||
|
||||
interface PieceData<T extends string> {
|
||||
// Keep the canonical public shape. Schema-derived author typings are separate.
|
||||
readonly piece: PublicWebPiece & { readonly type: T };
|
||||
}
|
||||
|
||||
export interface PiecePageProps<T extends string = string> {
|
||||
readonly data: PieceData<T> & { readonly tags: readonly PieceTag[] };
|
||||
readonly helpers: TemplateHelpers;
|
||||
readonly components: PageComponents;
|
||||
}
|
||||
|
||||
export interface PieceIconOptions {
|
||||
readonly size: TemplateSize;
|
||||
readonly lazy: boolean;
|
||||
readonly active: boolean;
|
||||
}
|
||||
|
||||
export interface PieceIconProps<T extends string = string> {
|
||||
readonly data: PieceData<T>;
|
||||
readonly options: PieceIconOptions;
|
||||
readonly helpers: TemplateHelpers;
|
||||
}
|
||||
|
||||
export interface PieceOpengraphProps<T extends string = string> {
|
||||
readonly data: PieceData<T>;
|
||||
readonly options: { readonly size: TemplateSize };
|
||||
readonly helpers: TemplateHelpers;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { PublicWebPiece } from '@luzzle/web.pieces';
|
||||
import type { PageComponents } from '../components/props.js';
|
||||
import type { TemplateHelpers } from '../helpers.js';
|
||||
|
||||
export interface RootPageProps {
|
||||
readonly data: {
|
||||
readonly types: readonly string[];
|
||||
readonly pieces: readonly PublicWebPiece[];
|
||||
};
|
||||
readonly helpers: TemplateHelpers;
|
||||
readonly components: PageComponents;
|
||||
}
|
||||
|
||||
export interface FeedReference {
|
||||
readonly url: string;
|
||||
readonly type: 'piece' | 'tag';
|
||||
readonly label: string;
|
||||
}
|
||||
|
||||
export interface FeedPageProps {
|
||||
readonly data: {
|
||||
readonly feed: FeedReference;
|
||||
readonly pieces: readonly PublicWebPiece[];
|
||||
};
|
||||
readonly helpers: TemplateHelpers;
|
||||
readonly components: PageComponents;
|
||||
}
|
||||
|
||||
export interface ErrorPageProps {
|
||||
readonly data: {
|
||||
readonly status: number;
|
||||
/** Safe public text, never a private debugging message or stack trace. */
|
||||
readonly message: string;
|
||||
};
|
||||
readonly helpers: TemplateHelpers;
|
||||
readonly components: PageComponents;
|
||||
}
|
||||
@@ -25,14 +25,28 @@ test('manifest always selects built JS and the runtime installation has only loc
|
||||
const manifest = JSON.parse(await readFile(path.join(root, 'package.json'), 'utf8'));
|
||||
assert.equal(manifest.dependencies, undefined);
|
||||
assert.equal(manifest.peerDependencies, undefined);
|
||||
const typeExports = {
|
||||
'./pieces/props': './dist/pieces/props.d.ts',
|
||||
'./site/props': './dist/site/props.d.ts',
|
||||
'./components/props': './dist/components/props.d.ts',
|
||||
'./helpers': './dist/helpers.d.ts'
|
||||
};
|
||||
for (const [name, conditions] of Object.entries(manifest.exports)) {
|
||||
if (name === './compatibility.json') continue;
|
||||
if (Object.hasOwn(typeExports, name)) {
|
||||
assert.deepEqual(conditions, { types: typeExports[name] });
|
||||
assert.ok((await readFile(path.join(root, conditions.types), 'utf8')).length);
|
||||
await assert.rejects(import(`${manifest.name}/${name.slice(2)}`), {
|
||||
code: 'ERR_PACKAGE_PATH_NOT_EXPORTED'
|
||||
});
|
||||
continue;
|
||||
}
|
||||
assert.equal(conditions.development, conditions.default);
|
||||
assert.match(conditions.default, /^\.\/dist\/.*\.js$/);
|
||||
}
|
||||
const dist = path.join(root, 'dist');
|
||||
for (const filename of await files(dist)) {
|
||||
assert.match(filename, /\.(js|json)$/); // no compiler/toolchain sources or sourcemaps
|
||||
assert.match(filename, /\.(js|json)$|\.d\.ts$/); // declarations, never executable TS or maps
|
||||
if (!filename.endsWith('.js')) continue;
|
||||
const code = await readFile(path.join(dist, filename), 'utf8');
|
||||
assert.ok(!code.includes(root), 'absolute checkout path entered a runtime chunk');
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
/**
|
||||
* Compile-only tests: TypeScript checks every function body; nothing executes.
|
||||
* Accepted scenarios must compile. Each @ts-expect-error marks a rejected input
|
||||
* that must remain invalid. Exporting the scenarios avoids unused-variable noise.
|
||||
*/
|
||||
import type { PublicWebPiece, PieceIconPalette } from '@luzzle/web.pieces';
|
||||
import type {
|
||||
PiecePageProps,
|
||||
PieceIconProps,
|
||||
PieceIconInputs,
|
||||
PieceIconOptions,
|
||||
PieceOpengraphProps
|
||||
} from '@luzzle/web.templates/pieces/props';
|
||||
import type {
|
||||
RootPageProps,
|
||||
FeedPageProps,
|
||||
FeedReference,
|
||||
ErrorPageProps
|
||||
} from '@luzzle/web.templates/site/props';
|
||||
import type { NavBannerInputs, PageComponents } from '@luzzle/web.templates/components/props';
|
||||
import type { TemplateHelpers } from '@luzzle/web.templates/helpers';
|
||||
|
||||
// Typed stand-ins, not real archive records. Both type names are arbitrary.
|
||||
declare const piece: PublicWebPiece & { type: 'custom-kind' };
|
||||
declare const otherPiece: PublicWebPiece & { type: 'another-kind' };
|
||||
declare const helpers: TemplateHelpers;
|
||||
declare const components: PageComponents;
|
||||
|
||||
// Accepted template inputs -----------------------------------------------------
|
||||
|
||||
export function acceptsIconCallWithoutOptionalDefaults(): PieceIconInputs<'custom-kind'> {
|
||||
return { piece, size: { width: 80 } };
|
||||
}
|
||||
|
||||
export function acceptsIconPropsWithResolvedOptions(): PieceIconProps<'custom-kind'> {
|
||||
return {
|
||||
data: { piece },
|
||||
options: { size: { width: 80, height: 120 }, lazy: false, active: true },
|
||||
helpers
|
||||
};
|
||||
}
|
||||
|
||||
export function acceptsPiecePageWithNormalizedTags(): PiecePageProps<'custom-kind'> {
|
||||
return {
|
||||
data: { piece, tags: [{ tag: 'quick note', slug: 'quick-note' }] },
|
||||
helpers,
|
||||
components
|
||||
};
|
||||
}
|
||||
|
||||
export function acceptsOpengraphProps(): PieceOpengraphProps<'another-kind'> {
|
||||
return {
|
||||
data: { piece: otherPiece },
|
||||
options: { size: { width: 1200, height: 630 } },
|
||||
helpers
|
||||
};
|
||||
}
|
||||
|
||||
export function acceptsRootPageWithMixedPieceTypes(): RootPageProps {
|
||||
return {
|
||||
data: { types: ['custom-kind', 'another-kind'], pieces: [piece, otherPiece] },
|
||||
helpers,
|
||||
components
|
||||
};
|
||||
}
|
||||
|
||||
export function acceptsTagFeed(): FeedPageProps {
|
||||
return {
|
||||
data: {
|
||||
feed: { url: '/tags/quick-note', type: 'tag', label: 'Quick note' },
|
||||
pieces: [piece, otherPiece]
|
||||
},
|
||||
helpers,
|
||||
components
|
||||
};
|
||||
}
|
||||
|
||||
export function acceptsPublicErrorPage(): ErrorPageProps {
|
||||
return { data: { status: 404, message: 'Not found' }, helpers, components };
|
||||
}
|
||||
|
||||
export function preservesCanonicalPieceShape(page: PiecePageProps): PublicWebPiece {
|
||||
return page.data.piece;
|
||||
}
|
||||
|
||||
export function helpersReturnExpectedValues(): {
|
||||
url: string;
|
||||
palette: PieceIconPalette | undefined;
|
||||
image: string | undefined;
|
||||
asset: string | undefined;
|
||||
content: string | undefined;
|
||||
staticAsset: string | undefined;
|
||||
} {
|
||||
const bound = helpers.forPiece(piece);
|
||||
return {
|
||||
url: bound.getUrl(),
|
||||
palette: bound.getPalette(),
|
||||
image: bound.getImageUrl(null, 80, 'avif'),
|
||||
asset: bound.getAssetUrl('attachment', 'image.original'),
|
||||
content: bound.getAssetContent('body', 'markdown'),
|
||||
staticAsset: helpers.getStaticAssetUrl('images/logo.svg')
|
||||
};
|
||||
}
|
||||
|
||||
// Rejected icon inputs --------------------------------------------------------
|
||||
|
||||
export function rejectsIconCallWithoutSize(): PieceIconInputs {
|
||||
// @ts-expect-error Every icon component call requires a size.
|
||||
return { piece };
|
||||
}
|
||||
|
||||
export function rejectsIconCallWithWrongPieceType(): PieceIconInputs<'custom-kind'> {
|
||||
// @ts-expect-error The requested type must match the supplied piece.
|
||||
return { piece: otherPiece, size: { width: 80 } };
|
||||
}
|
||||
|
||||
export function rejectsAuthoredIconWithoutResolvedHeight(): PieceIconOptions {
|
||||
// @ts-expect-error Authored icons require height; only callers may omit it.
|
||||
return { size: { width: 80 }, lazy: false, active: false };
|
||||
}
|
||||
|
||||
export function rejectsAuthoredIconWithoutDefaults(): PieceIconOptions {
|
||||
// @ts-expect-error Authored icons require explicit lazy and active values.
|
||||
return { size: { width: 80, height: 120 } };
|
||||
}
|
||||
|
||||
// Props unavailable to a role --------------------------------------------------
|
||||
|
||||
export function rejectsLegacyFlatPieceProp(page: PiecePageProps): unknown {
|
||||
// @ts-expect-error The piece belongs under data.piece, not page.piece.
|
||||
return page.piece;
|
||||
}
|
||||
|
||||
export function rejectsPageOptions(page: PiecePageProps): unknown {
|
||||
// @ts-expect-error Piece pages have no options prop.
|
||||
return page.options;
|
||||
}
|
||||
|
||||
export function rejectsIconComponents(icon: PieceIconProps): unknown {
|
||||
// @ts-expect-error Icons cannot request navigation or nested piece icons.
|
||||
return icon.components;
|
||||
}
|
||||
|
||||
export function rejectsSeparateIconTags(icon: PieceIconProps): unknown {
|
||||
// @ts-expect-error Icons have no separate tags prop.
|
||||
return icon.tags;
|
||||
}
|
||||
|
||||
export function rejectsOpengraphComponents(opengraph: PieceOpengraphProps): unknown {
|
||||
// @ts-expect-error Open Graph images receive no page components.
|
||||
return opengraph.components;
|
||||
}
|
||||
|
||||
// Rejected content and helper inputs ------------------------------------------
|
||||
|
||||
export function rejectsDatabaseEncodedTags(): PiecePageProps {
|
||||
return {
|
||||
data: {
|
||||
piece,
|
||||
// @ts-expect-error Page tags must be normalized { tag, slug } records.
|
||||
tags: ['quick note']
|
||||
},
|
||||
helpers,
|
||||
components
|
||||
};
|
||||
}
|
||||
|
||||
export function rejectsPieceTypeAsFeedKind(): FeedReference {
|
||||
// @ts-expect-error Feed kind is 'piece' or 'tag', not a piece type name.
|
||||
return { url: '/feed', type: 'custom-kind', label: 'Feed' };
|
||||
}
|
||||
|
||||
export function rejectsCustomNavigationSlots(): NavBannerInputs {
|
||||
// @ts-expect-error Navigation does not accept custom snippet slots.
|
||||
return { items: {} };
|
||||
}
|
||||
|
||||
export function rejectsUnsupportedImageFormat(): string | undefined {
|
||||
// @ts-expect-error Image formats are limited to jpg, avif, webp, and png.
|
||||
return helpers.forPiece(piece).getImageUrl('cover', 80, 'gif');
|
||||
}
|
||||
|
||||
export function rejectsNumericAssetKey(): string | undefined {
|
||||
// @ts-expect-error Asset keys must be strings (or null/undefined for images).
|
||||
return helpers.forPiece(piece).getImageUrl(12, 80, 'jpg');
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "./tsconfig.types.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"noEmit": true,
|
||||
"emitDeclarationOnly": false,
|
||||
"noCheck": false,
|
||||
"noUnusedLocals": true
|
||||
},
|
||||
"include": [
|
||||
"src/components/props.ts",
|
||||
"src/pieces/props.ts",
|
||||
"src/site/props.ts",
|
||||
"src/helpers.ts",
|
||||
"test/types/**/*.ts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"strict": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"isolatedDeclarations": true,
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"declarationMap": false,
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"types": [],
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": [
|
||||
"src/components/props.ts",
|
||||
"src/pieces/props.ts",
|
||||
"src/site/props.ts",
|
||||
"src/helpers.ts"
|
||||
]
|
||||
}
|
||||
Generated
+9
@@ -682,6 +682,9 @@ importers:
|
||||
'@eslint/js':
|
||||
specifier: 'catalog:'
|
||||
version: 9.22.0
|
||||
'@luzzle/web.pieces':
|
||||
specifier: workspace:*
|
||||
version: link:../pieces
|
||||
'@rollup/plugin-node-resolve':
|
||||
specifier: 16.0.3
|
||||
version: 16.0.3(rollup@4.60.4)
|
||||
@@ -715,6 +718,12 @@ importers:
|
||||
svelte:
|
||||
specifier: 5.55.9
|
||||
version: 5.55.9(@typescript-eslint/types@8.19.1)
|
||||
typescript:
|
||||
specifier: 'catalog:'
|
||||
version: 5.7.3
|
||||
typescript-eslint:
|
||||
specifier: 'catalog:'
|
||||
version: 8.19.1(eslint@9.22.0)(typescript@5.7.3)
|
||||
|
||||
packages/web/theme:
|
||||
dependencies:
|
||||
|
||||
Reference in new issue
Block a user