first commit
Copilot Setup Steps / copilot-setup-steps (push) Has been cancelled

This commit is contained in:
2026-04-22 19:51:20 +07:00
commit 93d1b7c3d3
579 changed files with 99797 additions and 0 deletions
+42
View File
@@ -0,0 +1,42 @@
module.exports = {
extends: [
'plugin:ghost/ts',
'plugin:react/recommended',
'plugin:react-hooks/recommended'
],
plugins: [
'ghost',
'react-refresh',
'tailwindcss'
],
settings: {
react: {
version: 'detect'
}
},
rules: {
// suppress errors for missing 'import React' in JSX files, as we don't need it
'react/react-in-jsx-scope': 'off',
// ignore prop-types for now
'react/prop-types': 'off',
'no-restricted-imports': ['error', {
paths: [{
name: '@tryghost/shade',
message: 'Import from layered subpaths instead (components/primitives/patterns/utils/app/tokens).'
}]
}],
'react/jsx-sort-props': ['error', {
reservedFirst: true,
callbacksLast: true,
shorthandLast: true,
locale: 'en'
}],
'react/button-has-type': 'error',
'react/no-array-index-key': 'error',
'react/jsx-key': 'off',
// Enforce kebab-case (lowercase with hyphens) for all filenames
'ghost/filenames/match-regex': ['error', '^[a-z0-9.-]+$', false]
}
};
+2
View File
@@ -0,0 +1,2 @@
dist
types
+22
View File
@@ -0,0 +1,22 @@
# Admin X Framework
Ghost Shared Framework that is used by all the micro-frontends for common functionality like data fetching and routing.
## Pre-requisites
- Run `pnpm` in Ghost monorepo root
## Develop
This is a monorepo package.
Follow the instructions for the top-level repo.
1. `git clone` this repo & `cd` into it as usual
2. Run `pnpm` to install top-level dependencies.
## Test
- `pnpm lint` - run just eslint
- `pnpm test` - runs acceptance tests
In package.json you can find other related running options too.
+128
View File
@@ -0,0 +1,128 @@
{
"name": "@tryghost/admin-x-framework",
"type": "module",
"version": "0.0.0",
"repository": "https://github.com/TryGhost/Ghost/tree/main/apps/admin-x-framework",
"author": "Ghost Foundation",
"private": true,
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs",
"types": "./types/index.d.ts"
},
"./errors": {
"import": "./dist/errors.js",
"require": "./dist/errors.cjs",
"types": "./types/errors.d.ts"
},
"./helpers": {
"import": "./dist/helpers.js",
"require": "./dist/helpers.cjs",
"types": "./types/helpers.d.ts"
},
"./hooks": {
"import": "./dist/hooks.js",
"require": "./dist/hooks.cjs",
"types": "./types/hooks.d.ts"
},
"./routing": {
"import": "./dist/routing.js",
"require": "./dist/routing.cjs",
"types": "./types/routing.d.ts"
},
"./api/*": {
"import": "./dist/api/*.js",
"require": "./dist/api/*.cjs",
"types": "./types/api/*.d.ts"
},
"./utils/post-utils": {
"import": "./dist/utils/post-utils.js",
"require": "./dist/utils/post-utils.cjs",
"types": "./types/utils/post-utils.d.ts"
},
"./vite": {
"import": "./dist/vite.js",
"require": "./dist/vite.cjs",
"types": "./types/vite.d.ts"
},
"./playwright": {
"import": "./dist/playwright.js",
"require": "./dist/playwright.cjs",
"types": "./types/playwright.d.ts"
},
"./test/*": {
"import": "./dist/test/*.js",
"require": "./dist/test/*.cjs",
"types": "./types/test/*.d.ts"
}
},
"sideEffects": false,
"scripts": {
"dev": "vite build --watch",
"build": "tsc -p tsconfig.declaration.json && vite build",
"test": "pnpm test:types && pnpm test:unit",
"test:types": "tsc --noEmit",
"test:unit": "vitest run --coverage",
"lint:code": "eslint --ext .js,.ts,.cjs,.tsx src/ --cache",
"lint": "pnpm lint:code && pnpm lint:test",
"lint:test": "eslint -c test/.eslintrc.cjs --ext .js,.ts,.cjs,.tsx test/ --cache"
},
"files": [
"dist",
"types"
],
"devDependencies": {
"@playwright/test": "1.59.1",
"@testing-library/jest-dom": "5.17.0",
"@testing-library/react": "14.3.1",
"@tryghost/koenig-lexical": "1.7.30",
"@types/react": "18.3.28",
"@types/react-dom": "18.3.7",
"@vitejs/plugin-react": "4.7.0",
"@vitest/coverage-v8": "^1.6.1",
"c8": "10.1.3",
"eslint": "catalog:",
"eslint-plugin-react-hooks": "4.6.2",
"eslint-plugin-react-refresh": "0.4.24",
"glob": "^10.5.0",
"jsdom": "28.1.0",
"msw": "2.12.14",
"sinon": "18.0.1",
"typescript": "5.9.3",
"vite": "5.4.21",
"vite-plugin-css-injected-by-js": "3.5.2",
"vite-plugin-svgr": "3.3.0",
"vitest": "1.6.1"
},
"dependencies": {
"@ebay/nice-modal-react": "1.2.13",
"@sentry/react": "7.120.4",
"@tanstack/react-query": "4.36.1",
"@tinybirdco/charts": "0.2.4",
"@tryghost/admin-x-design-system": "workspace:*",
"@tryghost/shade": "workspace:*",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-hot-toast": "2.6.0",
"react-router": "7.14.0"
},
"peerDependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"nx": {
"targets": {
"build": {
"dependsOn": [
"^build"
]
},
"test:unit": {
"dependsOn": [
"^build"
]
}
}
}
}
+2
View File
@@ -0,0 +1,2 @@
export * from './utils/errors';
+2
View File
@@ -0,0 +1,2 @@
export * from './utils/helpers';
+10
View File
@@ -0,0 +1,10 @@
export {default as useFilterableApi} from './hooks/use-filterable-api';
export {default as useForm} from './hooks/use-form';
export type {Dirtyable, ErrorMessages, FormHook, OkProps, SaveHandler, SaveState} from './hooks/use-form';
export {default as useHandleError} from './hooks/use-handle-error';
export {usePermission} from './hooks/use-permissions';
export {useKoenigFileUpload, koenigFileUploadTypes} from './hooks/use-koenig-file-upload';
export {useKoenigFetchEmbed} from './hooks/use-koenig-fetch-embed';
export type {KoenigFileUploadType} from './hooks/use-koenig-file-upload';
export {useKoenigLinkSuggestions} from './hooks/use-koenig-link-suggestions';
export {usePinturaConfig} from './hooks/use-pintura-config';
+58
View File
@@ -0,0 +1,58 @@
// Framework
export type {StatsConfig, FrameworkContextType, FrameworkProviderProps, TopLevelFrameworkProps} from './providers/framework-provider';
export {FrameworkProvider, useFramework} from './providers/framework-provider';
// App Context
export type {AppSettings, BaseAppProps, AppContextType, AppProviderProps} from './providers/app-provider';
export {AppContext, AppProvider, useAppContext} from './providers/app-provider';
// Hooks
export {useActiveVisitors} from './hooks/use-active-visitors';
export {default as useForm} from './hooks/use-form';
export type {Dirtyable, ErrorMessages, FormHook, OkProps, SaveHandler, SaveState} from './hooks/use-form';
export {default as useHandleError} from './hooks/use-handle-error';
export {default as useFilterableApi} from './hooks/use-filterable-api';
export {useTinybirdToken} from './hooks/use-tinybird-token';
export type {UseTinybirdTokenResult} from './hooks/use-tinybird-token';
export {useTinybirdQuery} from './hooks/use-tinybird-query';
export type {UseTinybirdQueryOptions} from './hooks/use-tinybird-query';
export {useKoenigFileUpload, koenigFileUploadTypes} from './hooks/use-koenig-file-upload';
export {useKoenigFetchEmbed} from './hooks/use-koenig-fetch-embed';
export type {KoenigFileUploadType} from './hooks/use-koenig-file-upload';
export {useKoenigLinkSuggestions} from './hooks/use-koenig-link-suggestions';
// Currency utilities
export {getSymbol} from './utils/currency';
// Stats utilities
export {getStatEndpointUrl, getToken} from './utils/stats-config';
// Post utilities
export type {Post} from './api/posts';
export {hasBeenEmailed} from './utils/post-utils';
export {isEmailOnly, isPublishedOnly, isPublishedAndEmailed, getPostMetricsToDisplay} from './utils/post-helpers';
export {focusKoenigEditorOnBottomClick} from './utils/focus-koenig-editor-on-bottom-click';
// Source utilities
export {SOURCE_DOMAIN_MAP, getFaviconDomain, extractDomain, isDomainOrSubdomain, processSources, extendSourcesWithPercentages, normalizeSource} from './utils/source-utils';
export type {BaseSourceData, ProcessedSourceData, ExtendSourcesOptions} from './utils/source-utils';
// Routing
export type {RouteObject} from 'react-router';
export type {RouterProviderProps, NavigateOptions} from './providers/router-provider';
export {RouterProvider, useNavigate, useBaseRoute, useRouteHasParams, resetScrollPosition, ScrollRestoration, Navigate} from './providers/router-provider';
export {useNavigationStack} from './providers/navigation-stack-provider';
export {Link, NavLink, Outlet, useLocation, useParams, useSearchParams, redirect, matchRoutes, matchPath, useMatch, useMatches} from 'react-router';
// Lazy component loader
export {lazyComponent} from './utils/lazy-component';
// Data fetching
export type {InfiniteData} from '@tanstack/react-query';
export {useQueryClient} from '@tanstack/react-query';
// API
export type {TinybirdToken, TinybirdTokenResponseType} from './api/tinybird';
export {getTinybirdToken} from './api/tinybird';
export type {FeaturebaseToken, FeaturebaseTokenResponseType} from './api/featurebase';
export {getFeaturebaseToken} from './api/featurebase';
+62
View File
@@ -0,0 +1,62 @@
import {defineConfig, devices, PlaywrightTestConfig} from '@playwright/test';
export const E2E_PORT = 5173;
export function adminXPlaywrightConfig(overrides: Partial<PlaywrightTestConfig> = {}) {
/**
* See https://playwright.dev/docs/test-configuration.
*/
return defineConfig({
testDir: './test/acceptance',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Hardcode to use all cores in CI */
workers: process.env.CI ? '100%' : (process.env.PLAYWRIGHT_SLOWMO ? 1 : undefined),
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
baseURL: `http://localhost:${E2E_PORT}`,
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
launchOptions: {
slowMo: parseInt(process.env.PLAYWRIGHT_SLOWMO ?? '') || 0,
// force GPU hardware acceleration
// (even in headless mode)
args: ['--use-gl=egl']
}
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: {...devices['Desktop Chrome']}
},
...(process.env.ALL_BROWSERS ? [{
name: 'firefox',
use: {...devices['Desktop Firefox']}
},
{
name: 'webkit',
use: {...devices['Desktop Safari']}
}] : [])
],
/* Run local dev server before starting the tests */
webServer: {
command: `pnpm dev:start`,
url: `http://localhost:${E2E_PORT}`,
reuseExistingServer: !process.env.CI,
timeout: 10000
},
...overrides
});
}
+3
View File
@@ -0,0 +1,3 @@
export {RoutingProvider, useRouteChangeCallback, useRouting} from './providers/routing-provider';
export type {ExternalLink, InternalLink, ModalComponent, RoutingModalProps} from './providers/routing-provider';
+88
View File
@@ -0,0 +1,88 @@
import react from '@vitejs/plugin-react';
import {PluginOption, UserConfig, mergeConfig} from 'vite';
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js';
import svgr from 'vite-plugin-svgr';
import {defineConfig} from 'vitest/config';
const externalPlugin = ({externals}: { externals: Record<string, string> }): PluginOption => {
return {
name: 'external-globals',
apply: 'build',
enforce: 'pre',
resolveId(id) {
if (Object.keys(externals).includes(id)) {
// Naming convention for IDs that will be resolved by a plugin
return `\0${id}`;
}
},
async load(id) {
const [originalId, externalName] = Object.entries(externals).find(([key]) => id === `\0${key}`) || [];
if (originalId) {
const module = await import(originalId);
return Object.keys(module).map(key => (key === 'default' ? `export default ${externalName};` : `export const ${key} = ${externalName}.${key};`)).join('\n');
}
}
};
};
// https://vitejs.dev/config/
export default function adminXViteConfig({packageName, entry, overrides}: {packageName: string; entry: string; overrides?: UserConfig}) {
const outputFileName = packageName[0] === '@' ? packageName.slice(packageName.indexOf('/') + 1) : packageName;
const defaultConfig = defineConfig({
logLevel: process.env.CI ? 'info' : 'warn',
plugins: [
svgr(),
react(),
externalPlugin({
externals: {
react: 'React',
'react-dom': 'ReactDOM'
}
}),
cssInjectedByJsPlugin() as PluginOption // Cast to avoid type conflicts
],
define: {
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
'process.env.VITEST_SEGFAULT_RETRY': 3,
'import.meta.env.GHOST_BUILD_VERSION': JSON.stringify(process.env.GHOST_BUILD_VERSION || '')
},
preview: {
port: 4174
},
build: {
reportCompressedSize: false,
minify: true,
sourcemap: true,
lib: {
formats: ['es'],
entry,
name: packageName,
fileName(format) {
if (format === 'umd') {
return `${outputFileName}.umd.js`;
}
return `${outputFileName}.js`;
}
},
commonjsOptions: {
include: [/packages/, /node_modules/]
}
},
test: {
globals: true, // required for @testing-library/jest-dom extensions
environment: 'jsdom',
include: ['./test/unit/**/*'],
testTimeout: process.env.TIMEOUT ? parseInt(process.env.TIMEOUT) : 10000,
...(process.env.CI && { // https://github.com/vitest-dev/vitest/issues/1674
minThreads: 1,
maxThreads: 2
})
}
});
return mergeConfig(defaultConfig, overrides || {});
};
+12
View File
@@ -0,0 +1,12 @@
module.exports = {
plugins: ['ghost'],
extends: [
'plugin:ghost/ts-test'
],
rules: {
'ghost/mocha/no-mocha-arrows': 'off',
'@typescript-eslint/no-explicit-any': 'off',
// Enforce kebab-case (lowercase with hyphens) for all filenames
'ghost/filenames/match-regex': ['error', '^[a-z0-9.-]+$', false]
}
};
+4
View File
@@ -0,0 +1,4 @@
/// <reference types="vitest/globals" />
import '@testing-library/jest-dom';
// This file ensures TypeScript knows about vitest globals
@@ -0,0 +1,15 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": false,
"composite": true,
"declaration": true,
"declarationMap": true,
"declarationDir": "./types",
"emitDeclarationOnly": true,
"tsBuildInfoFile": "./types/tsconfig.tsbuildinfo",
"rootDir": "./src"
},
"include": ["src"],
"exclude": ["src/**/*.stories.tsx", "src/**/*.test.ts", "src/**/*.test.tsx"]
}
+24
View File
@@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "ESNext",
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"module": "ESNext",
"skipLibCheck": true,
"types": ["vite/client", "vitest/globals"],
/* Bundler mode */
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src", "test"],
"references": [{ "path": "./tsconfig.node.json" }]
}
+10
View File
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts", "package.json"]
}
+68
View File
@@ -0,0 +1,68 @@
import path from 'path';
import react from '@vitejs/plugin-react';
import {globSync} from 'glob';
import {resolve} from 'path';
import {defineConfig} from 'vitest/config';
// https://vitejs.dev/config/
export default (function viteConfig() {
return defineConfig({
logLevel: process.env.CI ? 'info' : 'warn',
plugins: [
react()
],
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
}
},
preview: {
port: 4174
},
build: {
reportCompressedSize: false,
minify: false,
sourcemap: true,
outDir: 'dist',
lib: {
formats: ['es', 'cjs'],
entry: globSync(resolve(__dirname, 'src/**/*.{ts,tsx}')).reduce((entries, libpath) => {
if (libpath.endsWith('.d.ts')) {
return entries;
}
const outPath = libpath.replace(resolve(__dirname, 'src') + '/', '').replace(/\.(ts|tsx)$/, '');
entries[outPath] = libpath;
return entries;
}, {} as Record<string, string>)
},
commonjsOptions: {
include: [/packages/, /node_modules/]
},
rollupOptions: {
external: (source) => {
if (source.startsWith('.')) {
return false;
}
if (source.includes('node_modules')) {
return true;
}
return !source.includes(__dirname);
}
}
},
test: {
globals: true, // required for @testing-library/jest-dom extensions
environment: 'jsdom',
include: ['./test/unit/**/*'],
setupFiles: ['./test/setup.ts'],
testTimeout: process.env.TIMEOUT ? parseInt(process.env.TIMEOUT) : 10000,
...(process.env.CI && { // https://github.com/vitest-dev/vitest/issues/1674
minThreads: 1,
maxThreads: 2
})
}
});
});