chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:38:36 +08:00
commit 8e2a6eb840
10194 changed files with 1593658 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
{
"name": "@nx/nx-dev-data-access-courses",
"version": "0.0.1",
"type": "commonjs",
"private": true,
"main": "./src/index.ts",
"types": "./src/index.ts",
"dependencies": {
"@nx/nx-dev-ui-markdoc": "workspace:*",
"@nx/nx-dev-data-access-documents": "workspace:*"
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"name": "data-access-courses",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "nx-dev/data-access-courses/src",
"projectType": "library",
"targets": {},
"tags": []
}
+2
View File
@@ -0,0 +1,2 @@
export * from './lib/courses.api';
export * from './lib/course.types';
@@ -0,0 +1,25 @@
import type { BlogAuthor } from '@nx/nx-dev-data-access-documents/node-only';
export interface Course {
id: string;
title: string;
description: string;
content: string;
authors: BlogAuthor[];
repository?: string;
lessons: Lesson[];
lessonCount?: number;
filePath: string;
externalLink?: string;
totalDuration: string;
order?: number;
}
export interface Lesson {
id: string;
title: string;
description: string;
videoUrl: string;
duration: string;
filePath: string;
}
@@ -0,0 +1,115 @@
import { readFile, readdir } from 'fs/promises';
import { join } from 'path';
import { extractFrontmatter } from '@nx/nx-dev-ui-markdoc';
import { readFileSync, lstatSync } from 'fs';
import { Course, Lesson } from './course.types';
import { calculateTotalDuration } from './duration.utils';
export class CoursesApi {
constructor(
private readonly options: {
coursesRoot: string;
authorsPath: string;
}
) {
if (!options.coursesRoot) {
throw new Error('courses root cannot be undefined');
}
if (!options.authorsPath) {
throw new Error('authors path cannot be undefined');
}
}
async getAllCourses(): Promise<Course[]> {
const courseFolders = await readdir(this.options.coursesRoot);
const courses = await Promise.all(
courseFolders
.filter((directory) => {
const stat = lstatSync(join(this.options.coursesRoot, directory));
return stat.isDirectory();
})
.map((folder) => this.getCourse(folder))
);
return courses.sort((a, b) => {
// If both courses have order, sort by order
if (a.order !== undefined && b.order !== undefined) {
return a.order - b.order;
}
// If only one has order, prioritize the one with order
if (a.order !== undefined) return -1;
if (b.order !== undefined) return 1;
// If neither has order, sort by id (folder name)
return a.id.localeCompare(b.id);
});
}
async getCourse(folderName: string): Promise<Course> {
const authors = JSON.parse(readFileSync(this.options.authorsPath, 'utf8'));
const coursePath = join(this.options.coursesRoot, folderName);
const courseFilePath = join(coursePath, 'course.md');
const content = await readFile(courseFilePath, 'utf-8');
const frontmatter = extractFrontmatter(content);
let lessons: Lesson[] = [];
if (!frontmatter.externalLink) {
const lessonFolders = await readdir(coursePath);
const tmpLessons = await Promise.all(
lessonFolders
.filter((folder) => {
const stat = lstatSync(join(coursePath, folder));
return stat.isDirectory();
})
.map((folder) => this.getLessons(folderName, folder))
);
lessons = tmpLessons.flat();
}
return {
id: folderName,
title: frontmatter.title,
description: frontmatter.description,
content,
authors: authors.filter((author: { name: string }) =>
frontmatter.authors.includes(author.name)
),
repository: frontmatter.repository,
lessons,
filePath: courseFilePath,
totalDuration: calculateTotalDuration(lessons),
lessonCount: frontmatter.lessonCount,
externalLink: frontmatter.externalLink,
order: frontmatter.order,
};
}
private async getLessons(
courseId: string,
lessonFolder: string
): Promise<Lesson[]> {
const lessonPath = join(this.options.coursesRoot, courseId, lessonFolder);
const lessonFiles = await readdir(lessonPath);
const lessons = await Promise.all(
lessonFiles.map(async (file) => {
if (!file.endsWith('.md')) return null;
const filePath = join(lessonPath, file);
const content = await readFile(filePath, 'utf-8');
const frontmatter = extractFrontmatter(content);
if (!frontmatter || !frontmatter.title) {
throw new Error(`Lesson ${lessonFolder}/${file} has no title`);
}
return {
id: `${lessonFolder}-${file.replace('.md', '')}`,
title: frontmatter.title,
description: content,
videoUrl: frontmatter.videoUrl || null,
duration: frontmatter.duration || null,
filePath,
};
})
);
return lessons.filter((lesson): lesson is Lesson => lesson !== null);
}
}
@@ -0,0 +1,17 @@
import { Lesson } from './course.types';
export function calculateTotalDuration(lessons: Lesson[]): string {
const totalMinutes = lessons.reduce((total, lesson) => {
if (!lesson.duration) return total;
const [minutes, seconds] = lesson.duration.split(':').map(Number);
return total + minutes + seconds / 60;
}, 0);
const hours = Math.floor(totalMinutes / 60);
const minutes = Math.round(totalMinutes % 60);
if (hours > 0) {
return `${hours}h ${minutes}m`;
}
return `${minutes}m`;
}
+18
View File
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"jsx": "react-jsx",
"allowJs": false,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"ignoreDeprecations": "6.0"
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
}
],
"extends": "../../tsconfig.base.json"
}
@@ -0,0 +1,20 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"types": ["node"],
"composite": true,
"declaration": true,
"jsx": "react-jsx"
},
"references": [
{
"path": "../data-access-documents/tsconfig.lib.json"
},
{
"path": "../ui-markdoc/tsconfig.lib.json"
}
],
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"],
"include": ["src/**/*.ts"]
}
+12
View File
@@ -0,0 +1,12 @@
{
"presets": [
[
"@nx/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}
+11
View File
@@ -0,0 +1,11 @@
# documentation-api
This library provides the data necessary to display the documentation.
The [`data`](./src/data) folder contains the version mapping as well as a snapshot of the docs.
## Testing
```
nx test documentation-api
```
@@ -0,0 +1,3 @@
import { baseConfig } from '../../eslint.config.mjs';
export default [...baseConfig];
@@ -0,0 +1,24 @@
/* eslint-disable */
module.exports = {
displayName: 'nx-dev-data-access-documents',
globals: {},
transform: {
'^.+\\.[tj]sx?$': [
'ts-jest',
{
tsconfig: '<rootDir>/tsconfig.spec.json',
},
],
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/nx-dev/data-access-documents',
preset: '../../jest.preset.js',
moduleNameMapper: {
// Override for nx-dev packages - point to packages directory
'^@nx/devkit$': '<rootDir>/../../packages/devkit/index.ts',
'^@nx/devkit/testing$': '<rootDir>/../../packages/devkit/testing.ts',
'^@nx/devkit/internal-testing-utils$':
'<rootDir>/../../packages/devkit/internal-testing-utils.ts',
'^@nx/devkit/src/(.*)$': '<rootDir>/../../packages/devkit/src/$1',
},
};
@@ -0,0 +1 @@
export * from '../src/node.index';
+24
View File
@@ -0,0 +1,24 @@
{
"name": "@nx/nx-dev-data-access-documents",
"version": "0.0.1",
"type": "commonjs",
"private": true,
"exports": {
".": {
"import": "./src/index.ts",
"default": "./src/index.ts"
},
"./node-only": {
"import": "./src/node.index.ts",
"default": "./src/node.index.ts"
},
"./package.json": "./package.json"
},
"dependencies": {
"@nx/devkit": "workspace:*",
"@nx/nx-dev-models-document": "workspace:*",
"@nx/nx-dev-models-package": "workspace:*",
"@nx/nx-dev-ui-markdoc": "workspace:*"
},
"devDependencies": {}
}
@@ -0,0 +1,8 @@
{
"name": "nx-dev-data-access-documents",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "nx-dev/data-access-documents/src",
"projectType": "library",
"targets": {},
"tags": ["scope:nx-dev", "type:data-access"]
}
@@ -0,0 +1,182 @@
import { readFileSync, accessSync, constants } from 'fs';
import { join, basename, parse, resolve } from 'path';
import { extractFrontmatter } from '@nx/nx-dev-ui-markdoc';
import { sortPosts } from './blog.util';
import { BlogPostDataEntry } from './blog.model';
import { readFile, readdir } from 'fs/promises';
export class BlogApi {
constructor(
private readonly options: {
id: string;
blogRoot: string;
}
) {
if (!options.id) {
throw new Error('id cannot be undefined');
}
if (!options.blogRoot) {
throw new Error('public blog root cannot be undefined');
}
}
async getBlogTags(): Promise<string[]> {
const blogs = await this.getBlogs();
const tags = new Set<string>();
blogs.forEach((blog) => {
blog.tags.forEach((tag) => tags.add(tag));
});
return Array.from(tags);
}
async getBlogs(
filterFn?: (post: BlogPostDataEntry) => boolean
): Promise<BlogPostDataEntry[]> {
return await this.getAllBlogs(filterFn);
}
async getAllBlogs(
filterFn?: (post: BlogPostDataEntry) => boolean
): Promise<BlogPostDataEntry[]> {
const files: string[] = await readdir(this.options.blogRoot);
const authors = JSON.parse(
readFileSync(join(this.options.blogRoot, 'authors.json'), 'utf8')
);
const allPosts: BlogPostDataEntry[] = [];
for (const file of files) {
const filePath = join(this.options.blogRoot, file);
if (!filePath.endsWith('.md')) continue;
const content = await readFile(filePath, 'utf8');
const frontmatter = extractFrontmatter(content);
const slug = this.calculateSlug(filePath, frontmatter);
const { image, type } = this.determineOgImage(frontmatter.cover_image);
const post = {
content,
title: frontmatter.title ?? null,
description: frontmatter.description ?? null,
authors: authors.filter((author) =>
frontmatter.authors?.includes(author.name)
),
eventDate: this.dateFromFileName(file),
date: this.calculateDate(file, frontmatter),
time: frontmatter.time,
status: frontmatter.status,
cover_image: frontmatter.cover_image
? `/documentation${frontmatter.cover_image}` // Match the prefix used by markdown parser
: null,
tags: frontmatter.tags ?? [],
reposts: frontmatter.reposts ?? [],
// Do not default to 'false' so you can 'unpin' a blog post
pinned: frontmatter.pinned ?? null,
ogImage: image,
ogImageType: type,
filePath,
slug,
youtubeUrl: frontmatter.youtubeUrl,
registrationUrl: frontmatter.registrationUrl,
podcastYoutubeId: frontmatter.podcastYoutubeId,
podcastSpotifyId: frontmatter.podcastSpotifyId,
podcastIHeartUrl: frontmatter.podcastIHeartUrl,
podcastAppleUrl: frontmatter.podcastAppleUrl,
podcastAmazonUrl: frontmatter.podcastAmazonUrl,
published: frontmatter.published ?? true,
metrics: frontmatter.metrics,
hideCoverImage: frontmatter.hideCoverImage ?? false,
};
const isDevelopment = process.env.NODE_ENV === 'development';
const shouldIncludePost = !frontmatter.draft || isDevelopment;
if (shouldIncludePost && (!filterFn || filterFn(post))) {
allPosts.push(post);
}
}
return sortPosts(allPosts);
}
// Optimize this so we don't read the FS multiple times
async getBlogPostBySlug(
slug: string | null
): Promise<BlogPostDataEntry | undefined> {
if (!slug) throw new Error(`Could not find blog post with slug: ${slug}`);
return (await this.getBlogs()).find((post) => post.slug === slug);
}
private calculateSlug(filePath: string, frontmatter: any): string {
const baseName = basename(filePath, '.md');
return frontmatter.slug || baseName;
}
private dateFromFileName(filename: string): string {
const timeString = new Date().toISOString().split('T')[1];
const regexp = /^(\d\d\d\d-\d\d-\d\d).+$/;
const match = filename.match(regexp);
if (match) {
return new Date(match[1] + ' ' + timeString).toISOString();
} else {
throw new Error(`Could not parse date from filename: ${filename}`);
}
}
private calculateDate(filename: string, frontmatter: any): string {
const date: Date = new Date();
const timeString = date.toISOString().split('T')[1];
if (frontmatter.date) {
return new Date(
frontmatter.date.toISOString().split('T')[0] + 'T' + timeString
).toISOString();
} else {
return this.dateFromFileName(filename);
}
}
private fileExists(filePath: string): boolean {
try {
accessSync(filePath, constants.F_OK);
return true;
} catch (error) {
return false;
}
}
private determineOgImage(imagePath: string): {
image: string;
type: string;
} {
const allowedExtensions = ['.png', '.webp', '.jpg', '.jpeg'];
const defaultImage = 'https://nx.dev/socials/nx-media.png';
const defaultType = 'png';
if (!imagePath) {
return { image: defaultImage, type: defaultType };
}
const { ext } = parse(imagePath);
if (!allowedExtensions.includes(ext)) {
const foundExt = allowedExtensions.find((allowedExt) => {
const ogImagePath = imagePath.replace(ext, allowedExt);
return this.fileExists(
join(
'public',
'documentation',
resolve(this.options.blogRoot, ogImagePath)
)
);
});
if (!foundExt) {
return { image: defaultImage, type: defaultType };
}
return {
image: join('documentation', imagePath.replace(ext, foundExt)),
type: foundExt.replace('.', ''),
};
}
return {
image: join('documentation', imagePath),
type: ext.replace('.', ''),
};
}
}
@@ -0,0 +1,32 @@
export type BlogPostDataEntry = {
title: string;
content: string;
description: string;
authors: BlogAuthor[];
date: string;
cover_image: string | null;
tags: string[];
reposts: string[];
updated?: string;
pinned?: boolean;
filePath: string;
slug: string;
youtubeUrl?: string;
podcastYoutubeId?: string;
podcastSpotifyId?: string;
podcastAmazonUrl?: string;
podcastAppleUrl?: string;
podcastIHeartUrl?: string;
published?: boolean;
ogImage?: string;
ogImageType?: string;
metrics?: Array<{ value: string; label: string }>;
hideCoverImage?: boolean;
};
export type BlogAuthor = {
name: string;
image: string;
twitter: string;
github: string;
};
@@ -0,0 +1,72 @@
import { sortPosts } from './blog.util';
describe('sortPosts', () => {
test('sort from latest to earliest', () => {
const posts = [
createPost({ date: '2022-01-01', title: 'Post 1', slug: 'post-1' }),
createPost({ date: '2023-01-01', title: 'Post 2', slug: 'post-2' }),
];
const results = sortPosts(posts);
expect(results.map((p) => p.slug)).toEqual(['post-2', 'post-1']);
});
test('latest pinned posts are presented first', () => {
const posts = [
createPost({
date: '2023-01-01',
title: 'Post 1',
slug: 'post-1',
pinned: true,
}),
createPost({ date: '2023-02-01', title: 'Post 2', slug: 'post-2' }),
createPost({
date: '2023-03-01',
title: 'Post 3',
slug: 'post-3',
pinned: true,
}),
createPost({
date: '2023-04-01',
title: 'Post 4',
slug: 'post-4',
pinned: true,
}),
createPost({ date: '2023-05-01', title: 'Post 5', slug: 'post-5' }),
];
const results = sortPosts(posts);
expect(results.map((p) => p.slug)).toEqual([
'post-4',
'post-3',
'post-1',
'post-5',
'post-2',
]);
});
});
function createPost(data: {
date: string;
title: string;
slug: string;
pinned?: boolean;
}) {
return {
content: '',
filePath: `${data.slug}.md`,
slug: data.slug,
date: data.date,
title: data.title,
description: '',
authors: [],
ogImage: '',
ogImageType: '',
cover_image: '',
tags: [],
reposts: [],
pinned: data.pinned ?? undefined,
};
}
@@ -0,0 +1,9 @@
import { BlogPostDataEntry } from './blog.model';
export function sortPosts(posts: BlogPostDataEntry[]): BlogPostDataEntry[] {
return posts.sort((a, b) => {
if (a.pinned === true && b.pinned !== true) return -1;
if (b.pinned === true && a.pinned !== true) return 1;
return new Date(b.date).getTime() - new Date(a.date).getTime();
});
}
@@ -0,0 +1,39 @@
import { readFileSync, readdirSync } from 'fs';
import { join } from 'path';
export interface ChangelogRawContentEntry {
version: string;
content: string;
filePath: string;
}
export class ChangelogApi {
constructor(
private readonly options: {
id: string;
changelogRoot: string;
}
) {
if (!options.id) {
throw new Error('id cannot be undefined');
}
if (!options.changelogRoot) {
throw new Error('public docs root cannot be undefined');
}
}
getChangelogEntries(): ChangelogRawContentEntry[] {
const files = readdirSync(this.options.changelogRoot);
return files.map((file) => {
const filePath = join(this.options.changelogRoot, file);
const content = readFileSync(filePath, 'utf8');
const version = file.replace('.md', '').replace(/_/g, '.');
return {
content,
version,
filePath,
};
});
}
}
@@ -0,0 +1,391 @@
import {
pkgToGeneratedApiDocs,
type DocumentMetadata,
type ProcessedDocument,
type RelatedDocument,
} from '@nx/nx-dev-models-document';
import { type ProcessedPackageMetadata } from '@nx/nx-dev-models-package';
import { existsSync, readdirSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
import { type TagsApi } from './tags.api';
import { extractFrontmatter } from '@nx/nx-dev-ui-markdoc';
interface StaticDocumentPaths {
params: { segments: string[] };
}
export class DocumentsApi {
private readonly manifest: Record<string, DocumentMetadata>;
private readonly packagesManifest?: Record<string, ProcessedPackageMetadata>;
constructor(
private readonly options: {
id: string;
manifest: Record<string, DocumentMetadata>;
packagesManifest?: Record<string, ProcessedPackageMetadata>;
prefix: string;
publicDocsRoot: string;
tagsApi: TagsApi;
}
) {
if (!options.id) {
throw new Error('id cannot be undefined');
}
if (!options.prefix) {
options.prefix = '';
}
if (!options.publicDocsRoot) {
throw new Error('public docs root cannot be undefined');
}
if (!options.manifest) {
throw new Error('public document sources cannot be undefined');
}
this.manifest = structuredClone(this.options.manifest);
if (this.options.packagesManifest) {
this.packagesManifest = structuredClone(this.options.packagesManifest);
}
if (
options.id === 'angular-rspack-documents' ||
options.id === 'angular-rsbuild-documents'
) {
this.manifest = Object.assign(
this.manifest,
this.getAngularRspackPackage()
);
}
}
private getManifestKey(path: string): string {
return '/' + path;
}
// TODO(colum): Remove this once we move angular rspack into main repo (when stable).
getAngularRspackPackage(): Record<string, DocumentMetadata> {
return {
'/nx-api/angular-rspack/documents/create-config': {
id: 'create-config',
name: 'createConfig',
description: 'createConfig for @nx/angular-rspack',
path: '/nx-api/angular-rspack/documents/create-config',
file: 'shared/guides/angular-rspack/api/nx-angular-rspack/create-config',
isExternal: false,
itemList: [],
tags: [],
},
'/nx-api/angular-rspack/documents/create-server': {
id: 'create-server',
name: 'createServer',
description: 'createServer for @nx/angular-rspack',
path: '/nx-api/angular-rspack/documents/create-server',
file: 'shared/guides/angular-rspack/api/nx-angular-rspack/create-server',
isExternal: false,
itemList: [],
tags: [],
},
'/nx-api/angular-rsbuild/documents/create-config': {
id: 'create-config',
name: 'createConfig',
description: 'createConfig for @nx/angular-rsbuild',
path: '/nx-api/angular-rspack/documents/create-config',
file: 'shared/guides/angular-rspack/api/nx-angular-rsbuild/create-config',
isExternal: false,
itemList: [],
tags: [],
},
'/nx-api/angular-rsbuild/documents/create-server': {
id: 'create-server',
name: 'createServer',
description: 'createServer for @nx/angular-rsbuild',
path: '/nx-api/angular-rspack/documents/create-server',
file: 'shared/guides/angular-rspack/api/nx-angular-rsbuild/create-server',
isExternal: false,
itemList: [],
tags: [],
},
};
}
getFilePath(path: string): string {
return join(this.options.publicDocsRoot, path);
}
getParamsStaticDocumentPaths(): StaticDocumentPaths[] {
return Object.keys(this.manifest).map((path) => ({
params: {
segments: !!this.options.prefix
? [this.options.prefix].concat(path.split('/').filter(Boolean).flat())
: path.split('/').filter(Boolean).flat(),
},
}));
}
getSlugsStaticDocumentPaths(): string[] {
let paths: string[] = [];
// Add regular document paths
if (this.options.prefix) {
paths = Object.keys(this.manifest).map(
(path) => `/${this.options.prefix}` + path
);
} else {
paths = Object.keys(this.manifest);
}
// API Docs
if (this.packagesManifest) {
const packages = Object.values(this.packagesManifest);
packages.forEach((pkg) => {
const apiDocData = pkgToGeneratedApiDocs[pkg.name];
// Packages are not mapped, skip creating URLs for them.
if (!apiDocData) return;
const apiPagePath = apiDocData.pagePath;
paths.push(apiPagePath);
if (apiDocData.includeDocuments) {
paths.push(`${apiPagePath}/documents`);
Object.keys(pkg.documents).forEach((path) => {
paths.push(path);
});
// Legacy devkit API documents
if (pkg.name === 'devkit') {
readdirSync('../../docs/generated/devkit').forEach((fileName) => {
// Private files
if (fileName.startsWith('.')) return;
if (fileName.endsWith('.md')) {
const apiDocPath = `${
pkgToGeneratedApiDocs['devkit'].pagePath
}/documents/${fileName.replace('.md', '')}`;
paths.push(apiDocPath);
} else {
readdirSync('../../docs/generated/devkit/' + fileName).forEach(
(subFileName) => {
const apiDocPath = `${
pkgToGeneratedApiDocs['devkit'].pagePath
}/documents/${fileName}/${subFileName.replace('.md', '')}`;
paths.push(apiDocPath);
}
);
}
});
}
}
paths.push(`${apiPagePath}/executors`);
Object.keys(pkg.executors).forEach((path) => {
paths.push(path);
});
paths.push(`${apiPagePath}/generators`);
Object.keys(pkg.generators).forEach((path) => {
paths.push(path);
});
paths.push(`${apiPagePath}/migrations`);
});
}
return paths;
}
getDocument(path: string[]): ProcessedDocument {
const document: DocumentMetadata | null =
this.manifest[this.getManifestKey(path.join('/'))] || null;
if (!document) {
// Legacy handler for devkit docs
if (
path[0] === 'nx-api' &&
path[1] === 'devkit' &&
path[2] === 'documents'
) {
const file = `generated/devkit/${path.slice(3).join('/')}`;
return {
content: readFileSync(this.getFilePath(`${file}.md`), 'utf8'),
description: '',
filePath: this.getFilePath(file),
id: path.at(-1) || '',
name: path.at(-1) || '',
relatedDocuments: {},
tags: [],
};
}
// NEW Legacy handler for devkit docs
if (
path[0] === 'reference' &&
path[1] === 'core-api' &&
path[2] === 'devkit' &&
path[3] === 'documents'
) {
const file = `generated/devkit/${path.slice(4).join('/')}`;
return {
content: readFileSync(this.getFilePath(`${file}.md`), 'utf8'),
description: '',
filePath: this.getFilePath(file),
id: path.at(-1) || '',
name: path.at(-1) || '',
relatedDocuments: {},
tags: [],
};
}
throw new Error(
`Document not found in manifest with: "${path.join('/')}"`
);
}
if (this.isDocumentIndex(document)) return this.getDocumentIndex(path);
return {
content: readFileSync(this.getFilePath(`${document.file}.md`), 'utf8'),
description: document.description,
filePath: this.getFilePath(`${document.file}.md`),
id: document.id,
name: document.name,
mediaImage: document.mediaImage || '',
relatedDocuments: this.getRelatedDocuments(document.tags),
parentDocuments: path.map((segment, index): RelatedDocument => {
const parentPath = path.slice(0, index + 1).join('/');
const parentDocument =
this.manifest[this.getManifestKey(parentPath)] || null;
if (!parentDocument) {
return {
id: segment,
name: '',
description: '',
file: '',
path: '/' + path.slice(0, index + 1).join('/'),
};
}
return {
id: parentDocument.id,
name: parentDocument.name,
description: parentDocument.description,
file: parentDocument.file,
path: parentDocument.path,
};
}),
tags: document.tags,
};
}
getRelatedDocuments(tags: string[]): Record<string, RelatedDocument[]> {
const relatedDocuments = {};
tags.forEach(
(tag) =>
(relatedDocuments[tag] = this.options.tagsApi.getAssociatedItems(tag))
);
return relatedDocuments;
}
isDocumentIndex(document: DocumentMetadata): boolean {
return !!document.itemList.length || !document.file;
}
generateDocumentIndexTemplate(document: DocumentMetadata): string {
const cardsTemplate = document.itemList
.map((i) => {
// Try to get description from frontmatter first
let description = i.description ?? '';
// Get the document metadata to find the file path
// i.path might already have a leading slash or might not, so we need to check both
const itemDocument =
this.manifest[i.path] || this.manifest[this.getManifestKey(i.path)];
if (itemDocument && itemDocument.file) {
const filePath = this.getFilePath(`${itemDocument.file}.md`);
if (existsSync(filePath)) {
try {
const content = readFileSync(filePath, 'utf8');
const frontmatter = extractFrontmatter(content);
// Use frontmatter description if available, otherwise fall back to map.json description
description = frontmatter.description || description;
} catch (e) {
// If there's an error reading the file, fall back to the original description
console.warn(`Could not read frontmatter from ${filePath}:`, e);
}
}
}
return {
title: i.name,
description,
url: i.path,
};
})
.map(
(card) =>
`{% card title="${card.title}" description="${
card.description
}" url="${[this.options.prefix, card.url]
.filter(Boolean)
.join('/')}" /%}\n`
)
.join('');
return [
`# ${document.name}\n\n ${document.description ?? ''}\n\n`,
...(!document.itemList.length
? ['No items found.']
: ['{% cards %}\n', cardsTemplate, '{% /cards %}\n\n']),
].join('');
}
getDocumentIndex(path: string[]): ProcessedDocument {
const document: DocumentMetadata | null =
this.manifest[this.getManifestKey(path.join('/'))] || null;
if (!document)
throw new Error(
`Document not found in manifest with: "${path.join('/')}"`
);
if (!!document.file)
return {
content: readFileSync(this.getFilePath(`${document.file}.md`), 'utf8'),
description: document.description,
filePath: this.getFilePath(`${document.file}.md`),
id: document.id,
name: document.name,
relatedDocuments: this.getRelatedDocuments(document.tags),
tags: document.tags,
};
return {
content: this.generateDocumentIndexTemplate(document),
description: document.description,
filePath: '',
id: document.id,
name: document.name,
relatedDocuments: this.getRelatedDocuments(document.tags),
tags: document.tags,
};
}
generateRootDocumentIndex(options: {
name: string;
description: string;
}): ProcessedDocument {
const document = {
id: 'root',
name: options.name,
description: options.description,
file: '',
path: '',
isExternal: false,
itemList: Object.keys(this.manifest)
.filter((k) => k.split('/').length < 4) // Getting only top categories
.map((k) => this.manifest[k]),
tags: [],
};
return {
content: this.generateDocumentIndexTemplate(document),
description: document.description,
filePath: '',
id: document.id,
name: document.name,
relatedDocuments: this.getRelatedDocuments(document.tags),
tags: document.tags,
};
}
}
@@ -0,0 +1,16 @@
import { BlogApi } from './blog.api';
import { PodcastDataEntry } from './podcast.model';
export class PodcastApi {
_blogApi: BlogApi;
constructor(options: { blogApi: BlogApi }) {
this._blogApi = options.blogApi;
}
async getPodcastBlogs(): Promise<PodcastDataEntry[]> {
return await this._blogApi.getBlogs((post) =>
post.tags.map((t) => t.toLowerCase()).includes('podcast')
);
}
}
@@ -0,0 +1,5 @@
import { BlogPostDataEntry } from './blog.model';
export interface PodcastDataEntry extends BlogPostDataEntry {
duration?: string;
}
@@ -0,0 +1,35 @@
import { RelatedDocument } from '@nx/nx-dev-models-document';
export class TagsApi {
private readonly manifest: Record<string, RelatedDocument[]>;
constructor(private readonly tags: Record<string, RelatedDocument[]>) {
if (!tags) {
throw new Error('tags property cannot be undefined');
}
this.manifest = structuredClone(this.tags);
}
getAssociatedItems(tag: string): RelatedDocument[] {
const items: RelatedDocument[] | null = this.manifest[tag] || null;
if (!items) throw new Error(`No associated items found for tag: "${tag}"`);
return items;
}
getAssociatedItemsFromTags(tags: string[]): RelatedDocument[] {
return this.sortAndDeduplicateItems(
tags.map((t) => this.getAssociatedItems(t))
);
}
sortAndDeduplicateItems(tags: RelatedDocument[][]): RelatedDocument[] {
return tags.flat().reduce((acc, item: RelatedDocument) => {
if (acc.findIndex((curr) => curr.file === item.file) === -1) {
acc.push(item);
}
return acc;
}, [] as RelatedDocument[]);
}
}
@@ -0,0 +1,16 @@
import { BlogApi } from './blog.api';
import { WebinarDataEntry } from './webinar.model';
export class WebinarApi {
_blogApi: BlogApi;
constructor(options: { blogApi: BlogApi }) {
this._blogApi = options.blogApi;
}
async getWebinarBlogs(): Promise<WebinarDataEntry[]> {
return await this._blogApi.getBlogs((post) =>
post.tags.map((t) => t.toLowerCase()).includes('webinar')
);
}
}
@@ -0,0 +1,8 @@
import { BlogPostDataEntry } from './blog.model';
export interface WebinarDataEntry extends BlogPostDataEntry {
status?: 'Upcoming' | 'Past - Gated' | 'Past - Ungated';
eventDate?: string;
time?: string;
registrationUrl?: string;
}
@@ -0,0 +1,10 @@
export * from './lib/documents.api';
export * from './lib/changelog.api';
export * from './lib/blog.util';
export * from './lib/blog.api';
export * from './lib/blog.model';
export * from './lib/tags.api';
export * from './lib/podcast.model';
export * from './lib/podcast.api';
export * from './lib/webinar.model';
export * from './lib/webinar.api';
@@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"strictNullChecks": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
}
]
}
@@ -0,0 +1,29 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"outDir": "dist",
"types": ["node"],
"composite": true,
"declaration": true,
"jsx": "react-jsx"
},
"references": [
{
"path": "../ui-markdoc/tsconfig.lib.json"
},
{
"path": "../models-package/tsconfig.lib.json"
},
{
"path": "../models-document/tsconfig.lib.json"
},
{
"path": "../../packages/devkit/tsconfig.lib.json"
}
],
"include": ["**/*.ts"],
"exclude": ["**/*.spec.ts", "**/*.test.ts", "jest.config.ts"]
}
@@ -0,0 +1,22 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"outDir": "dist/spec",
"types": ["jest", "node"]
},
"include": [
"**/*.spec.ts",
"**/*.test.ts",
"**/*.spec.tsx",
"**/*.test.tsx",
"**/*.spec.js",
"**/*.test.js",
"**/*.spec.jsx",
"**/*.test.jsx",
"**/*.d.ts",
"jest.config.ts"
]
}
+12
View File
@@ -0,0 +1,12 @@
{
"presets": [
[
"@nx/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}
+7
View File
@@ -0,0 +1,7 @@
# nx-dev-feature-ai
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test nx-dev-feature-ai` to execute the unit tests via [Jest](https://jestjs.io).
+4
View File
@@ -0,0 +1,4 @@
import { baseConfig, reactHooksV7Off } from '../../eslint.config.mjs';
import nx from '@nx/eslint-plugin';
export default [...baseConfig, ...nx.configs['flat/react'], ...reactHooksV7Off];
+14
View File
@@ -0,0 +1,14 @@
module.exports = {
displayName: 'nx-dev-feature-ai',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/nx-dev/feature-ai',
preset: '../../jest.preset.js',
moduleNameMapper: {
// Override for nx-dev packages - point to packages directory
'^@nx/devkit$': '<rootDir>/../../packages/devkit/index.ts',
'^@nx/devkit/testing$': '<rootDir>/../../packages/devkit/testing.ts',
'^@nx/devkit/internal-testing-utils$':
'<rootDir>/../../packages/devkit/internal-testing-utils.ts',
'^@nx/devkit/src/(.*)$': '<rootDir>/../../packages/devkit/src/$1',
},
};
+17
View File
@@ -0,0 +1,17 @@
{
"name": "@nx/nx-dev-feature-ai",
"version": "0.0.1",
"type": "commonjs",
"private": true,
"main": "./src/index.ts",
"types": "./src/index.ts",
"dependencies": {
"@nx/nx-dev-util-ai": "workspace:*",
"@nx/nx-dev-ui-primitives": "workspace:*",
"@heroicons/react": "catalog:",
"@markdoc/markdoc": "0.2.2",
"ai": "3.0.19",
"react": "catalog:react",
"react-textarea-autosize": "catalog:"
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"name": "nx-dev-feature-ai",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "nx-dev/feature-ai/src",
"projectType": "library",
"targets": {},
"tags": ["scope:nx-dev", "type:feature"]
}
+1
View File
@@ -0,0 +1 @@
export * from './lib/feed-container';
@@ -0,0 +1,64 @@
import { type JSX, memo } from 'react';
import {
ExclamationTriangleIcon,
XCircleIcon,
} from '@heroicons/react/24/outline';
import Link from 'next/link';
function ErrorMessage({ error }: { error: any }): JSX.Element {
try {
if (error.message) {
error = JSON.parse(error.message);
console.error('Error: ', error);
}
} catch (e) {}
if (error?.data?.no_results) {
return (
<div className="rounded-md bg-yellow-50 p-4">
<div className="flex">
<div className="flex-shrink-0">
<ExclamationTriangleIcon
className="h-5 w-5 text-yellow-500"
aria-hidden="true"
/>
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-yellow-800">
No results found
</h3>
<div className="mt-2 text-sm text-yellow-700">
Sorry, I don't know how to help with that. You can visit the{' '}
<Link
href="https://nx.dev/getting-started/intro"
className="underline"
>
Nx documentation
</Link>{' '}
for more info.
</div>
</div>
</div>
</div>
);
} else {
return (
<div className="rounded-md bg-red-50 p-4">
<div className="flex">
<div className="flex-shrink-0">
<XCircleIcon className="h-5 w-5 text-red-400" aria-hidden="true" />
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-red-800">
Oopsies! I encountered an error
</h3>
<div className="mt-2 text-sm text-red-700">{error['message']}</div>
</div>
</div>
</div>
);
}
}
const MemoErrorMessage = memo(ErrorMessage);
export { MemoErrorMessage as ErrorMessage };
@@ -0,0 +1,154 @@
import {
type FormEvent,
type JSX,
RefObject,
useEffect,
useRef,
useState,
} from 'react';
import { ErrorMessage } from './error-message';
import { Feed } from './feed/feed';
import { LoadingState } from './loading-state';
import { Prompt } from './prompt';
import { storeQueryForUid } from '@nx/nx-dev-util-ai';
import { Message, useChat } from 'ai/react';
import { cx } from '@nx/nx-dev-ui-primitives';
const assistantWelcome: Message = {
id: 'first-custom-message',
role: 'assistant',
content:
"👋 Hi, I'm your Nx Assistant. With my ocean of knowledge about Nx, I can answer your questions and guide you to the relevant documentation. What would you like to know?",
};
export function FeedContainer(): JSX.Element {
const [error, setError] = useState<Error | null>(null);
const [startedReply, setStartedReply] = useState(false);
const [isStopped, setStopped] = useState(false);
const {
messages,
setMessages,
input,
handleInputChange,
handleSubmit: _handleSubmit,
stop,
reload,
isLoading,
} = useChat({
api: '/api/query-ai-handler',
onError: (error) => {
setError(error);
},
onResponse: () => {
setStartedReply(true);
setError(null);
},
onFinish: (response: Message) => {
setStartedReply(false);
storeQueryForUid(response.id, input);
},
});
/*
* Determine whether we should scroll to the bottom of new messages.
* Scroll if:
* 1. New message has come in (length > previous length)
* 2. User is close to the bottom of the messages
*
* Otherwise, user is probably reading messages, so don't scroll.
*/
const scrollableWrapperRef: RefObject<HTMLDivElement> | undefined =
useRef(null);
const currentMessagesLength = useRef(0);
useEffect(() => {
if (!scrollableWrapperRef.current) return;
const el = scrollableWrapperRef.current;
let shouldScroll = false;
if (messages.length > currentMessagesLength.current) {
currentMessagesLength.current = messages.length;
shouldScroll = true;
} else if (el.scrollTop + el.clientHeight + 50 >= el.scrollHeight) {
shouldScroll = true;
}
if (shouldScroll) el.scrollTo(0, el.scrollHeight);
}, [messages, isLoading]);
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
setStopped(false);
_handleSubmit(event);
};
const handleNewChat = () => {
setMessages([]);
setError(null);
setStartedReply(false);
setStopped(false);
};
const handleFeedback = () => {
// no-op: analytics removed
};
const handleStopGenerating = () => {
setStopped(true);
stop();
};
const handleRegenerate = () => {
setStopped(false);
reload();
};
return (
<>
{/*WRAPPER*/}
<div
ref={scrollableWrapperRef}
id="wrapper"
data-testid="wrapper"
className="relative flex flex-grow flex-col items-stretch justify-start overflow-y-scroll"
>
<div className="mx-auto w-full max-w-4xl grow items-stretch px-4 sm:px-8">
<div
id="content-wrapper"
className="w-full flex-auto flex-grow flex-col"
>
<div className="relative min-w-0 flex-auto">
{/*MAIN CONTENT*/}
<div data-document="main" className="relative pb-36">
<Feed
activity={!!messages.length ? messages : [assistantWelcome]}
onFeedback={handleFeedback}
/>
{/* Change this message if it's loading but it's writing as well */}
{isLoading && !startedReply && <LoadingState />}
{error && <ErrorMessage error={error} />}
<div
className={cx(
'left0 fixed bottom-0 right-0 w-full px-4 py-4 lg:px-0 lg:py-6',
'bg-gradient-to-t from-white via-white/75 dark:from-zinc-900 dark:via-zinc-900/75'
)}
>
<Prompt
onSubmit={handleSubmit}
onInputChange={handleInputChange}
onNewChat={handleNewChat}
onStopGenerating={handleStopGenerating}
onRegenerate={handleRegenerate}
input={input}
isGenerating={isLoading}
showNewChatCta={!isLoading && messages.length > 0}
showRegenerateCta={isStopped}
/>
</div>
</div>
</div>
</div>
</div>
</div>
</>
);
}
@@ -0,0 +1,19 @@
import { ComponentProps } from 'react';
export function ChatGptLogo(
props: ComponentProps<'svg'> & { title?: string; titleId?: string }
): JSX.Element {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 20 20"
fill="currentColor"
stroke="none"
{...props}
>
<path d="M18.6816 8.18531C18.8527 7.67776 18.94 7.14633 18.9401 6.61141C18.9401 5.72627 18.7011 4.85712 18.2478 4.09329C17.337 2.52895 15.6474 1.56315 13.8188 1.56315C13.4586 1.56315 13.0993 1.60069 12.7471 1.67514C12.2733 1.14844 11.6917 0.726808 11.0407 0.438063C10.3897 0.149318 9.68404 1.53941e-05 8.97029 0H8.93823L8.9262 6.97855e-05C6.71144 6.97855e-05 4.74733 1.41016 4.06649 3.48896C3.36173 3.63138 2.69594 3.92071 2.11369 4.33757C1.53143 4.75444 1.04615 5.28922 0.690321 5.90612C0.238418 6.67455 0.000276531 7.54714 0 8.43556C0.000172701 9.68414 0.469901 10.8882 1.31823 11.8147C1.14699 12.3222 1.05965 12.8537 1.05958 13.3886C1.05966 14.2737 1.29861 15.1429 1.75188 15.9067C2.2909 16.8327 3.11405 17.5659 4.10264 18.0005C5.09122 18.435 6.19415 18.5486 7.25237 18.3248C7.72623 18.8515 8.30786 19.2731 8.95891 19.5619C9.60995 19.8506 10.3156 20 11.0294 20H11.0615L11.0745 19.9999C13.2905 19.9999 15.2539 18.5898 15.9348 16.5091C16.6395 16.3666 17.3053 16.0773 17.8876 15.6604C18.4698 15.2435 18.9552 14.7087 19.311 14.0919C19.7624 13.3241 20.0001 12.4522 20 11.5646C19.9998 10.3161 19.5301 9.11202 18.6818 8.18559L18.6816 8.18531ZM11.0628 18.6925H11.0575C10.1708 18.6922 9.31227 18.3853 8.63118 17.8251C8.67162 17.8036 8.71159 17.7812 8.75105 17.758L12.787 15.4578C12.8877 15.4012 12.9714 15.3194 13.0297 15.2205C13.088 15.1217 13.1187 15.0094 13.1187 14.895V9.27708L14.8246 10.249C14.8336 10.2534 14.8413 10.2599 14.8471 10.2679C14.8529 10.276 14.8565 10.2853 14.8578 10.2951V14.9444C14.8555 17.0115 13.1579 18.6883 11.0628 18.6925ZM2.9014 15.2532C2.56803 14.6844 2.39239 14.039 2.39217 13.382C2.39217 13.1677 2.41114 12.9529 2.44808 12.7417C2.47808 12.7595 2.53045 12.791 2.56802 12.8123L6.60394 15.1125C6.70456 15.1705 6.81899 15.201 6.9355 15.201C7.05201 15.201 7.16643 15.1704 7.26702 15.1124L12.1945 12.3051V14.249L12.1945 14.2523C12.1945 14.2617 12.1923 14.2709 12.1881 14.2793C12.1838 14.2876 12.1777 14.2949 12.1701 14.3006L8.09017 16.6249C7.51288 16.9527 6.85851 17.1253 6.19244 17.1255C5.52566 17.1254 4.87062 16.9523 4.293 16.6237C3.71538 16.295 3.23547 15.8223 2.9014 15.253V15.2532ZM1.83963 6.55967C2.2829 5.79999 2.98282 5.21831 3.8169 4.91644C3.8169 4.95072 3.81492 5.01147 3.81492 5.05364V9.65413L3.81485 9.6579C3.81487 9.77213 3.84552 9.88432 3.9037 9.98308C3.96188 10.0818 4.04551 10.1636 4.1461 10.2202L9.07353 13.027L7.36772 13.9989C7.3593 14.0044 7.34965 14.0077 7.33961 14.0086C7.32957 14.0095 7.31947 14.008 7.31019 14.0041L3.22983 11.6778C2.65301 11.3481 2.17414 10.8746 1.84122 10.3048C1.5083 9.73494 1.33302 9.08877 1.33295 8.43102C1.3332 7.77431 1.50798 7.12914 1.83984 6.55988L1.83963 6.55967ZM15.8552 9.77779L10.9277 6.97059L12.6336 5.99906C12.642 5.99358 12.6517 5.99024 12.6617 5.98934C12.6718 5.98844 12.6819 5.99 12.6912 5.99389L16.7714 8.31819C17.3487 8.64738 17.8281 9.12062 18.1614 9.69041C18.4948 10.2602 18.6703 10.9065 18.6705 11.5644C18.6705 13.1346 17.6774 14.5397 16.1842 15.082V10.344C16.1844 10.3422 16.1844 10.3404 16.1844 10.3387C16.1844 10.2249 16.154 10.1131 16.0961 10.0146C16.0383 9.91613 15.9552 9.83444 15.8552 9.77779ZM17.5531 7.25638C17.5134 7.23239 17.4734 7.20889 17.4332 7.18585L13.3973 4.88558C13.2966 4.82772 13.1823 4.79722 13.0658 4.79718C12.9493 4.79722 12.835 4.82772 12.7343 4.88558L7.80682 7.69284V5.74902L7.80675 5.74567C7.80675 5.72667 7.81588 5.7088 7.83124 5.69742L11.9112 3.37508C12.4883 3.0468 13.1427 2.87398 13.8088 2.87395C15.9066 2.87395 17.6078 4.55252 17.6078 6.62238C17.6077 6.83479 17.5894 7.04681 17.5531 7.25617V7.25638ZM6.87936 10.7209L5.17313 9.74902C5.16417 9.74462 5.15646 9.7381 5.15067 9.73005C5.14488 9.72199 5.1412 9.71266 5.13994 9.70286V5.0535C5.14086 2.98476 6.84207 1.30759 8.93894 1.30759C9.82702 1.30777 10.687 1.61474 11.3697 2.17522C11.339 2.19177 11.2854 2.22096 11.2498 2.24225L7.21388 4.54246C7.11317 4.59899 7.02944 4.68082 6.97118 4.77963C6.91292 4.87845 6.88222 4.99072 6.8822 5.10503V5.10873L6.87936 10.7209ZM7.80604 8.74956L10.0006 7.49887L12.1952 8.74872V11.2493L10.0006 12.4992L7.80604 11.2493V8.74956Z" />
</svg>
);
}
@@ -0,0 +1,29 @@
import { normalizeContent } from './feed-answer';
jest.mock('@nx/nx-dev-ui-primitives', () => {
return {
cx: jest.fn(() => null),
};
});
describe('FeedAnswer', () => {
describe('normalizeContent', () => {
it('should normalize links to format expected by renderMarkdown', () => {
expect(
normalizeContent(`[!](https://nx.dev/shared/assets/image.png)`)
).toEqual(`[!](/shared/assets/image.png)`);
});
it('should escape numbers the beginning of lines to prevent numbered lists', () => {
expect(
normalizeContent(`
1. Hello
2. World
`)
).toEqual(`
1\\. Hello
2\\. World
`);
});
});
});
@@ -0,0 +1,124 @@
import {
HandThumbDownIcon,
HandThumbUpIcon,
} from '@heroicons/react/24/outline';
import { cx } from '@nx/nx-dev-ui-primitives';
import { useState } from 'react';
import { ChatGptLogo } from './chat-gpt-logo';
import { renderAiMarkdown } from '../render-markdown';
// Exported for tests
export function normalizeContent(content: string): string {
return (
content
// Prevents accidentally triggering numbered list.
.replace(/\n(\d)\./g, '\n$1\\.')
// The AI is prompted to replace relative links with absolute links (https://nx.dev/<path>).
// However, our docs renderer will prefix img src with `/documentation`, so we need to convert image links back to relative paths.
.replace(/\(https:\/\/nx.dev\/(.+?\.(png|svg|jpg|webp))\)/, '(/$1)')
);
}
export function FeedAnswer({
content,
feedbackButtonCallback,
isFirst,
}: {
content: string;
feedbackButtonCallback: (value: 'bad' | 'good') => void;
isFirst: boolean;
}) {
const [feedbackStatement, setFeedbackStatement] = useState<
'bad' | 'good' | null
>(null);
function handleFeedbackButtonClicked(statement: 'bad' | 'good'): void {
if (!!feedbackStatement) return;
setFeedbackStatement(statement);
feedbackButtonCallback(statement);
}
const normalizedContent = normalizeContent(content);
return (
<>
<div className="grid h-12 w-12 items-center justify-center rounded-full bg-white text-zinc-900 ring-1 ring-zinc-200 dark:bg-zinc-900 dark:text-white dark:ring-zinc-700">
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
className="h-8 w-8"
fill="currentColor"
>
<title>Nx</title>
<path d="M11.987 14.138l-3.132 4.923-5.193-8.427-.012 8.822H0V4.544h3.691l5.247 8.833.005-3.998 3.044 4.759zm.601-5.761c.024-.048 0-3.784.008-3.833h-3.65c.002.059-.005 3.776-.003 3.833h3.645zm5.634 4.134a2.061 2.061 0 0 0-1.969 1.336 1.963 1.963 0 0 1 2.343-.739c.396.161.917.422 1.33.283a2.1 2.1 0 0 0-1.704-.88zm3.39 1.061c-.375-.13-.8-.277-1.109-.681-.06-.08-.116-.17-.176-.265a2.143 2.143 0 0 0-.533-.642c-.294-.216-.68-.322-1.18-.322a2.482 2.482 0 0 0-2.294 1.536 2.325 2.325 0 0 1 4.002.388.75.75 0 0 0 .836.334c.493-.105.46.36 1.203.518v-.133c-.003-.446-.246-.55-.75-.733zm2.024 1.266a.723.723 0 0 0 .347-.638c-.01-2.957-2.41-5.487-5.37-5.487a5.364 5.364 0 0 0-4.487 2.418c-.01-.026-1.522-2.39-1.538-2.418H8.943l3.463 5.423-3.379 5.32h3.54l1.54-2.366 1.568 2.366h3.541l-3.21-5.052a.7.7 0 0 1-.084-.32 2.69 2.69 0 0 1 2.69-2.691h.001c1.488 0 1.736.89 2.057 1.308.634.826 1.9.464 1.9 1.541a.707.707 0 0 0 1.066.596zm.35.133c-.173.372-.56.338-.755.639-.176.271.114.412.114.412s.337.156.538-.311c.104-.231.14-.488.103-.74z" />
</svg>
</div>
<div className="min-w-0 flex-1">
<div>
<div className="flex items-center gap-2 text-lg text-zinc-900 dark:text-zinc-100">
Nx Assistant
</div>
<p className="mt-0.5 flex items-center gap-x-1 text-sm text-zinc-500">
<ChatGptLogo className="h-4 w-4 text-zinc-400" aria-hidden="true" />{' '}
AI powered
</p>
</div>
<div className="prose prose-zinc dark:prose-invert mt-2 w-full max-w-none 2xl:max-w-4xl">
{!isFirst && (
<div className="not-prose mb-4 rounded-md border border-amber-300 bg-amber-50 p-3 text-sm text-amber-900 dark:border-amber-800 dark:bg-amber-950 dark:text-amber-100">
<strong className="block font-semibold">
Always double-check!
</strong>
The results may not be accurate, so please always double check
with our documentation.
</div>
)}
{renderAiMarkdown(normalizedContent)}
</div>
{!isFirst && (
<div className="text-md group flex-1 gap-4 text-zinc-400 transition hover:text-zinc-500 md:flex md:items-center md:justify-end">
{feedbackStatement ? (
<p className="italic group-hover:flex">
{feedbackStatement === 'good'
? 'Glad I could help!'
: 'Sorry, could you please rephrase your question?'}
</p>
) : (
<p className="hidden italic group-hover:flex">
Is that the answer you were looking for?
</p>
)}
<div className="flex gap-4">
<button
className={cx(
'p-1 transition-all hover:rotate-12 hover:text-blue-500 disabled:cursor-not-allowed dark:hover:text-blue-500',
{ 'text-blue-500': feedbackStatement === 'bad' }
)}
disabled={!!feedbackStatement}
onClick={() => handleFeedbackButtonClicked('bad')}
title="Bad"
>
<span className="sr-only">Bad answer</span>
<HandThumbDownIcon className="h-6 w-6" aria-hidden="true" />
</button>
<button
className={cx(
'p-1 transition-all hover:rotate-12 hover:text-blue-500 disabled:cursor-not-allowed dark:hover:text-blue-500',
{ 'text-blue-500': feedbackStatement === 'good' }
)}
disabled={!!feedbackStatement}
onClick={() => handleFeedbackButtonClicked('good')}
title="Good"
>
<span className="sr-only">Good answer</span>
<HandThumbUpIcon className="h-6 w-6" aria-hidden="true" />
</button>
</div>
</div>
)}
</div>
</>
);
}
@@ -0,0 +1,9 @@
export function FeedQuestion({ content }: { content: string }) {
return (
<div className="flex w-full justify-end">
<p className="whitespace-pre-wrap break-words rounded-lg bg-blue-500 px-4 py-2 text-base text-white selection:bg-blue-900 dark:bg-blue-500">
{content}
</p>
</div>
);
}
+36
View File
@@ -0,0 +1,36 @@
import { FeedAnswer } from './feed-answer';
import { FeedQuestion } from './feed-question';
import { Message } from 'ai/react';
export function Feed({
activity,
onFeedback,
}: {
activity: Message[];
onFeedback: (statement: 'bad' | 'good', chatItemUid: string) => void;
}) {
return (
<div className="my-12 flow-root">
<ul role="list" className="-mb-8 space-y-12">
{activity.map((activityItem, activityItemIdx) => (
<li
key={[activityItem.role, activityItem.id].join('-')}
className="feed-item relative flex items-start space-x-3 pt-12"
>
{activityItem.role === 'assistant' ? (
<FeedAnswer
content={activityItem.content}
feedbackButtonCallback={(statement) =>
onFeedback(statement, activityItem.id)
}
isFirst={activityItemIdx === 0}
/>
) : (
<FeedQuestion content={activityItem.content} />
)}
</li>
))}
</ul>
</div>
);
}
@@ -0,0 +1,25 @@
import { ComponentProps } from 'react';
export function NrwlLogo(
props: ComponentProps<'svg'> & { title?: string; titleId?: string }
): JSX.Element {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10 18.9947C9.99253 18.996 9.98382 18.996 9.97444 18.996C9.89202 18.9986 9.8084 19 9.72598 19C8.82142 19 7.94802 18.8672 7.12155 18.6189C7.06659 18.6013 7.01233 18.5831 6.95618 18.563C6.69206 18.4746 6.43664 18.3686 6.18762 18.2482C5.80872 18.4706 5.38861 18.6267 4.93846 18.6982C4.83549 18.7138 4.73307 18.7256 4.62756 18.7327C4.42408 18.747 4.22497 18.7443 4.02885 18.7242L4.02766 18.7229C3.79167 18.6982 3.54877 18.693 3.30356 18.7099C2.8802 18.7392 2.47642 18.8316 2.09927 18.9785C2.04678 18.8367 2.01442 18.6845 2.00381 18.5258C1.94952 17.6754 2.56683 16.9405 3.38217 16.8838C3.68735 16.8624 3.96833 16.934 4.21425 17.0822C4.34793 17.1589 4.49765 17.2155 4.65502 17.2486C4.63257 17.2292 4.60944 17.209 4.58825 17.1894C4.58145 17.1843 4.57518 17.1784 4.56894 17.172C4.42774 17.049 4.29054 16.9197 4.15996 16.7831C3.96956 16.5951 3.73428 16.3382 3.50076 16.0046C3.48025 15.9772 3.45955 15.9481 3.43833 15.9187C3.38217 15.8395 3.32654 15.7601 3.27296 15.6782C3.01388 15.2835 2.78907 14.8614 2.60491 14.4178C2.5844 14.3691 2.56441 14.3204 2.54509 14.2695C2.48579 14.12 2.43207 13.9679 2.38271 13.8144C2.36582 13.7643 2.34964 13.713 2.33403 13.6609C2.30533 13.5634 2.2785 13.4652 2.25284 13.3669C2.24032 13.3182 2.22724 13.2694 2.21611 13.2187C2.18794 13.101 2.1629 12.9833 2.14116 12.8644C2.1224 12.7648 2.10432 12.6633 2.08922 12.5612C2.07996 12.4987 2.07125 12.4358 2.06378 12.3719C2.05616 12.3181 2.04992 12.2621 2.04431 12.2069C2.03931 12.156 2.03441 12.1073 2.02937 12.0573C2.02937 12.0475 2.02813 12.0384 2.02813 12.0292C2.02313 11.976 2.01943 11.9232 2.01685 11.8706C2.01252 11.8134 2.00882 11.7568 2.00747 11.6996C2.00258 11.5871 2 11.4733 2 11.3583C2 11.2918 2.00135 11.2275 2.00258 11.1625C2.00381 11.1079 2.00501 11.0538 2.00747 10.9998C2.00882 10.9693 2.01128 10.9388 2.01252 10.9081C2.01562 10.8632 2.01823 10.8185 2.02189 10.7748C2.02571 10.7098 2.03194 10.6461 2.03822 10.5817C2.04188 10.5375 2.04678 10.4939 2.0525 10.4491C2.0612 10.3711 2.07125 10.293 2.08242 10.2149C2.08676 10.1856 2.09057 10.1577 2.09546 10.1291C2.1017 10.09 2.10675 10.0517 2.11422 10.0133C2.12483 9.95156 2.13611 9.88923 2.14863 9.82861C2.16047 9.76624 2.17299 9.70504 2.1867 9.6433C2.19978 9.58606 2.2123 9.53081 2.22601 9.47358C2.33403 9.03397 2.47761 8.61061 2.65553 8.20742C2.67605 8.1594 2.69742 8.11322 2.71916 8.06704C2.82225 7.84332 2.93512 7.62598 3.05824 7.41604C3.11805 7.31262 3.18049 7.21189 3.24535 7.11299C3.31092 7.0121 3.37836 6.91265 3.44842 6.81519C3.51709 6.71956 3.58808 6.6259 3.65933 6.53299C3.70245 6.48156 3.74422 6.42829 3.78734 6.37686C3.82294 6.33593 3.85844 6.29367 3.89409 6.25259C3.93395 6.20784 3.97337 6.16349 4.01391 6.11871C4.05321 6.07509 4.0951 6.03159 4.13564 5.98922C4.14326 5.97946 4.15126 5.97168 4.15996 5.96386C4.22987 5.89106 4.30235 5.82079 4.37472 5.75064C4.89792 5.25181 5.49154 4.83231 6.13893 4.50988C6.18257 4.48708 6.22692 4.46424 6.27247 4.44483C6.48477 4.3427 6.70196 4.25286 6.9242 4.17353C6.97236 4.1561 7.01981 4.14038 7.06782 4.12423C7.09599 4.11501 7.12278 4.10595 7.15024 4.09816C7.19145 4.0833 7.23199 4.07155 7.27264 4.05976C7.34378 4.0384 7.41488 4.01825 7.48617 4C5.12258 4.70426 2.49192 6.95362 2.49192 10.875C2.49192 15.859 6.47348 16.824 8.32568 17.7201C9.44882 18.2624 9.85395 18.719 10 18.9947Z"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M9.90657 17.0587C9.85993 17.2489 9.66138 17.3641 9.46073 17.3143C9.26076 17.2662 9.13694 17.0722 9.18285 16.8819C9.22961 16.6924 9.38156 16.7738 9.58026 16.823C9.78076 16.871 9.95317 16.8697 9.90657 17.0587ZM6.00468 13.816C5.86286 13.9331 5.61197 13.8627 5.44584 13.6597C5.27917 13.4561 5.25872 13.1966 5.4005 13.0794C5.5416 12.9623 5.79134 13.0328 5.95931 13.2358C6.12529 13.4394 6.14647 13.6987 6.00468 13.816ZM4.77002 12.3326C4.63336 12.3652 4.48656 12.2307 4.44123 12.0355C4.39516 11.8402 4.46932 11.655 4.60595 11.6236C4.74261 11.5922 4.88956 11.7255 4.93489 11.9208C4.98012 12.1162 4.90611 12.3012 4.77002 12.3326ZM4.70878 10.4688C4.77767 10.4662 4.83833 10.5456 4.84223 10.6462C4.84667 10.7461 4.79311 10.8301 4.72285 10.8326C4.65381 10.8365 4.59314 10.757 4.5894 10.6565C4.58565 10.5561 4.63852 10.4726 4.70878 10.4688ZM5.33662 10.1602C5.35069 9.95901 5.47577 9.80538 5.61576 9.81431C5.7549 9.82519 5.85647 9.99556 5.84187 10.1966C5.8278 10.3971 5.70315 10.5507 5.56274 10.5406C5.42344 10.5317 5.32187 10.3606 5.33662 10.1602ZM17.5589 10.6148C17.5385 10.611 17.518 10.6071 17.497 10.6027C17.4695 10.5964 17.4421 10.59 17.414 10.5822C17.3871 10.5752 17.3597 10.5675 17.3328 10.5586C17.3321 10.5579 17.3316 10.5579 17.3309 10.5579C17.3136 10.5522 17.2964 10.5463 17.2791 10.5406C17.2453 10.5291 17.2115 10.5156 17.1775 10.5015C17.1775 10.5008 17.177 10.5015 17.177 10.5015C17.0927 10.4669 17.0096 10.4253 16.9299 10.3779C16.8283 10.3183 16.7312 10.2491 16.6411 10.1722C16.5734 10.1153 16.5102 10.0532 16.452 9.98717C16.4323 9.96541 16.4131 9.94295 16.3951 9.91992C16.2854 9.78346 16.1985 9.63233 16.1423 9.47162C15.7041 7.89474 14.7639 6.52609 13.5057 5.55255C12.2462 4.57835 10.6685 4 8.95494 4C8.32641 4 7.71512 4.07813 7.13139 4.22547C7.12374 4.22796 7.1174 4.22938 7.10975 4.23191C4.6915 4.92553 2 7.14088 2 11.003C2 15.9116 6.07364 16.8621 7.96868 17.7447C9.11779 18.2788 9.53244 18.7285 9.68172 19C10.9452 18.9647 12.1478 18.6842 13.2444 18.2033C13.3474 18.1551 13.4328 18.1001 13.5031 18.0406C13.5504 18.0732 13.6059 18.095 13.6648 18.1039L23 18.9839L13.8397 17.3457C13.8333 17.3443 13.827 17.3431 13.8219 17.3418C13.813 17.1099 13.6098 16.9563 13.5319 16.8505C13.4641 16.759 13.4616 16.6981 13.4616 16.6981C13.3977 15.7676 12.9781 14.9329 12.3387 14.3335C11.7678 13.7981 11.0243 13.447 10.1992 13.378V13.3766C10.111 13.2075 9.97809 13.0628 9.81708 12.9609C9.56482 12.763 9.40258 12.4434 9.40258 12.0969V12.0905C8.86671 12.1385 8.42993 12.5331 8.31804 13.0493C7.31733 12.5517 6.63008 11.5166 6.63008 10.3215C6.63008 9.49155 6.96025 8.73892 7.4968 8.19068C8.22106 7.42521 9.2296 6.96297 10.3632 6.96297C12.2033 6.96297 13.7504 8.21762 14.2012 9.91992C13.9087 10.2837 13.4604 10.5443 13.0158 10.6187C12.1906 10.7621 11.5417 11.4218 11.412 12.2532C12.1759 12.2532 12.6843 13.2984 14.1042 13.2984H14.1048C14.4586 13.2984 14.7787 13.1563 15.0117 12.9245C15.0341 12.902 15.0551 12.8796 15.0763 12.8558C15.0968 12.8321 15.1165 12.8078 15.135 12.7828C15.149 12.7643 15.1624 12.7451 15.1759 12.7252C15.1893 12.706 15.2015 12.6856 15.2136 12.6651C15.2379 12.6248 15.2595 12.5824 15.2794 12.5389C15.2858 12.5248 15.2922 12.5101 15.2979 12.4953L15.2986 12.4959C15.3107 12.5253 15.3241 12.5536 15.3375 12.5818C15.3515 12.6107 15.3669 12.6381 15.3828 12.6651C15.4072 12.706 15.4333 12.7451 15.4614 12.7828C15.471 12.795 15.4806 12.8078 15.4901 12.8201C15.51 12.8444 15.531 12.8681 15.5522 12.8904C15.5629 12.902 15.5739 12.9135 15.5847 12.9245C15.6849 13.0237 15.8006 13.1063 15.9283 13.1691C15.9564 13.1825 15.9852 13.1953 16.0146 13.2075C16.1628 13.2658 16.3237 13.2984 16.4922 13.2984C16.5191 13.2984 16.5453 13.2978 16.5709 13.2972C16.597 13.2966 16.6226 13.2952 16.6481 13.294C16.6737 13.2927 16.6992 13.2913 16.7242 13.2888C16.7464 13.287 16.7689 13.285 16.7906 13.2825C16.8372 13.2773 16.8825 13.2709 16.9272 13.2638C16.9464 13.2606 16.9655 13.2568 16.9841 13.2536C17.0032 13.2499 17.0218 13.246 17.0403 13.2421C17.0588 13.2383 17.0773 13.2344 17.0959 13.2299C17.1137 13.2255 17.1316 13.221 17.1496 13.2165C17.2031 13.2024 17.2549 13.187 17.3053 13.1703C17.3762 13.1466 17.4439 13.1211 17.5091 13.0929C17.5257 13.0858 17.5423 13.0788 17.5589 13.0704C17.5666 13.0672 17.5743 13.064 17.5819 13.0602C17.5889 13.0563 17.5967 13.0526 17.6043 13.0493C18.2596 12.7406 18.6627 12.2532 19.1845 12.2532C19.056 11.4295 18.4186 10.775 17.6043 10.6239L17.5589 10.6148Z"
/>
</svg>
);
}
@@ -0,0 +1,29 @@
export function LoadingState(): JSX.Element {
return (
<div className="flex w-full items-center justify-center gap-4 px-4 py-2 text-blue-500 transition duration-150 ease-in-out dark:text-blue-500">
<svg
className="h-5 w-5 animate-spin"
xmlns="http://www.w3.org/2000/svg"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
<p>Let me check the docs for you</p>
</div>
);
}
+144
View File
@@ -0,0 +1,144 @@
import { ChangeEvent, FormEvent, useEffect, useRef } from 'react';
import {
ArrowPathIcon,
PaperAirplaneIcon,
XMarkIcon,
StopIcon,
} from '@heroicons/react/24/outline';
import Textarea from 'react-textarea-autosize';
import { ChatRequestOptions } from 'ai';
import { cx } from '@nx/nx-dev-ui-primitives';
const controlButtonClasses = cx(
'inline-flex items-center gap-1.5 rounded-md border border-zinc-300 bg-white px-3 py-1.5 text-sm font-medium text-zinc-800 shadow-sm',
'hover:bg-zinc-50 focus:outline-none focus:ring-2 focus:ring-blue-500',
'dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-100 dark:hover:bg-zinc-800'
);
export function Prompt({
isGenerating,
showNewChatCta,
showRegenerateCta,
onSubmit,
onInputChange,
onNewChat,
onStopGenerating,
onRegenerate,
input,
}: {
isGenerating: boolean;
showNewChatCta: boolean;
showRegenerateCta: boolean;
onSubmit: (
e: FormEvent<HTMLFormElement>,
chatRequestOptions?: ChatRequestOptions | undefined
) => void;
onInputChange: (
e: ChangeEvent<HTMLTextAreaElement> | ChangeEvent<HTMLInputElement>
) => void;
onNewChat: () => void;
onStopGenerating: () => void;
onRegenerate: () => void;
input: string;
}) {
const formRef = useRef<HTMLFormElement>(null);
const inputRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
if (!isGenerating) inputRef.current?.focus();
}, [isGenerating]);
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
if (inputRef.current?.value.trim()) onSubmit(event);
else event.preventDefault();
};
const handleNewChat = () => {
onNewChat();
inputRef.current?.focus();
};
const handleStopGenerating = () => {
onStopGenerating();
inputRef.current?.focus();
};
return (
<form
ref={formRef}
onSubmit={handleSubmit}
className="relative mx-auto flex max-w-3xl gap-2 rounded-md border border-zinc-300 bg-white px-2 py-2 shadow-lg dark:border-zinc-900 dark:bg-zinc-700"
>
<div
className={cx(
'absolute -top-full left-1/2 mt-1 -translate-x-1/2',
'flex gap-4'
)}
>
{isGenerating && (
<button
type="button"
className={controlButtonClasses}
onClick={handleStopGenerating}
>
<StopIcon aria-hidden="true" className="h-5 w-5" />
<span className="text-base">Stop generating</span>
</button>
)}
{showNewChatCta && (
<button
type="button"
className={controlButtonClasses}
onClick={handleNewChat}
>
<XMarkIcon aria-hidden="true" className="h-5 w-5" />
<span className="text-base">Clear chat</span>
</button>
)}
{showRegenerateCta && (
<button
type="button"
className={controlButtonClasses}
onClick={onRegenerate}
>
<ArrowPathIcon aria-hidden="true" className="h-5 w-5" />
<span className="text-base">Regenerate</span>
</button>
)}
</div>
<div className="h-full max-h-[300px] w-full overflow-y-auto">
<Textarea
onKeyDown={(event) => {
if (
event.key === 'Enter' &&
!event.shiftKey &&
!event.nativeEvent.isComposing
) {
formRef.current?.requestSubmit();
event.preventDefault();
}
}}
ref={inputRef}
value={input}
onChange={onInputChange}
id="query-prompt"
name="query"
maxLength={500}
disabled={isGenerating}
className="block w-full resize-none border-none bg-transparent p-0 py-3 pl-2 text-sm placeholder-zinc-500 focus-within:outline-none focus:placeholder-zinc-400 focus:outline-none focus:ring-0 disabled:cursor-not-allowed dark:text-white dark:focus:placeholder-zinc-300"
placeholder="How does caching work?"
rows={1}
/>
</div>
<div className="flex">
<button
type="submit"
disabled={isGenerating}
className="inline-flex h-12 w-12 items-center justify-center self-end rounded-md bg-zinc-900 text-white hover:bg-zinc-800 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-white dark:text-zinc-900 dark:hover:bg-zinc-100"
>
<span className="sr-only">Ask</span>
<PaperAirplaneIcon aria-hidden="true" className="h-5 w-5" />
</button>
</div>
</form>
);
}
@@ -0,0 +1,13 @@
import markdoc from '@markdoc/markdoc';
import React, { ReactNode } from 'react';
const { parse, renderers, Tokenizer, transform } = markdoc;
const tokenizer = new Tokenizer({ allowComments: true });
export function renderAiMarkdown(content: string): ReactNode {
const tokens = tokenizer.tokenize(content);
const ast = parse(tokens);
const tree = transform(ast, {});
return renderers.react(tree, React);
}
@@ -0,0 +1,22 @@
import { InformationCircleIcon } from '@heroicons/react/24/outline';
export function ActivityLimitReached(): JSX.Element {
return (
<div className="rounded-md bg-zinc-50 p-4 shadow-xs ring-1 ring-zinc-100 dark:bg-zinc-800/40 dark:ring-zinc-700">
<div className="flex">
<div className="flex-shrink-0">
<InformationCircleIcon
className="h-5 w-5 text-zinc-500 dark:text-zinc-300"
aria-hidden="true"
/>
</div>
<div className="ml-3 flex-1 md:flex md:justify-between">
<p className="text-sm text-zinc-700 dark:text-zinc-400">
You've reached the maximum message history limit. Previous messages
will be removed.
</p>
</div>
</div>
</div>
);
}
@@ -0,0 +1,13 @@
import { ReactNode } from 'react';
export function SidebarContainer({ children }: { children: ReactNode[] }) {
return (
<div id="sidebar" data-testid="sidebar">
<div className="hidden h-full w-72 flex-col border-r border-zinc-200 md:flex dark:border-zinc-700 dark:bg-zinc-900">
<div className="relative flex flex-col gap-4 overflow-y-scroll p-4">
{...children}
</div>
</div>
</div>
);
}
@@ -0,0 +1,21 @@
import { ExclamationTriangleIcon } from '@heroicons/react/24/outline';
export function WarningMessage(): JSX.Element {
return (
<div className="rounded-md bg-yellow-50 p-4 ring-1 ring-yellow-100 dark:bg-yellow-900/30 dark:ring-yellow-900">
<h3 className="flex gap-x-3 text-sm font-medium text-yellow-600 dark:text-yellow-400">
<ExclamationTriangleIcon
className="h-5 w-5 text-yellow-500 dark:text-yellow-400"
aria-hidden="true"
/>{' '}
Always double check!
</h3>
<div className="mt-2 text-sm text-yellow-700 dark:text-yellow-600">
<p>
The results may not be accurate, so please always double check with
our documentation.
</p>
</div>
</div>
);
}
+22
View File
@@ -0,0 +1,22 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"jsx": "react-jsx",
"allowJs": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "bundler",
"module": "esnext"
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
}
]
}
+30
View File
@@ -0,0 +1,30 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"types": ["node"],
"lib": ["dom"],
"composite": true,
"declaration": true
},
"files": [
"../../node_modules/@nx/react/typings/cssmodule.d.ts",
"../../node_modules/@nx/react/typings/image.d.ts"
],
"exclude": [
"**/*.spec.ts",
"**/*.test.ts",
"**/*.spec.tsx",
"**/*.test.tsx",
"jest.config.ts"
],
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"],
"references": [
{
"path": "../ui-primitives/tsconfig.lib.json"
},
{
"path": "../util-ai/tsconfig.lib.json"
}
]
}
+19
View File
@@ -0,0 +1,19 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "dist/spec",
"types": ["jest", "node"]
},
"include": [
"**/*.spec.ts",
"**/*.test.ts",
"**/*.spec.tsx",
"**/*.test.tsx",
"**/*.spec.js",
"**/*.test.js",
"**/*.spec.jsx",
"**/*.test.jsx",
"**/*.d.ts",
"jest.config.ts"
]
}
+12
View File
@@ -0,0 +1,12 @@
{
"presets": [
[
"@nx/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}
+7
View File
@@ -0,0 +1,7 @@
# nx-dev-feature-analytics
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test nx-dev-feature-analytics` to execute the unit tests via [Jest](https://jestjs.io).
@@ -0,0 +1,4 @@
import { baseConfig, reactHooksV7Off } from '../../eslint.config.mjs';
import nx from '@nx/eslint-plugin';
export default [...baseConfig, ...nx.configs['flat/react'], ...reactHooksV7Off];
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
displayName: 'nx-dev-feature-analytics',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/nx-dev/feature-analytics',
preset: '../../jest.preset.js',
};
+8
View File
@@ -0,0 +1,8 @@
{
"name": "@nx/nx-dev-feature-analytics",
"version": "0.0.1",
"type": "commonjs",
"private": true,
"main": "./src/index.ts",
"types": "./src/index.ts"
}
+8
View File
@@ -0,0 +1,8 @@
{
"name": "nx-dev-feature-analytics",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "nx-dev/feature-analytics/src",
"projectType": "library",
"targets": {},
"tags": ["scope:nx-dev", "type:feature"]
}
+2
View File
@@ -0,0 +1,2 @@
export * from './lib/google-analytics';
export * from './lib/use-window-scroll-depth';
@@ -0,0 +1,42 @@
/**
* GA4 events are routed through GTM via dataLayer (no direct gtag calls here).
*/
declare global {
interface Window {
dataLayer?: Array<Record<string, unknown>>;
}
}
function pushGtmEvent(eventName: string, payload: Record<string, unknown>) {
if (process.env.NODE_ENV !== 'production') return;
if (typeof window === 'undefined') return;
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({ event: eventName, ...payload });
}
export function sendPageViewEventViaGtm(data: {
path?: string;
title?: string;
}): void {
pushGtmEvent('page_view', {
...(data.path ? { page_path: data.path } : {}),
...(data.title ? { page_title: data.title } : {}),
});
}
export function sendCustomEventViaGtm(
action: string,
category: string,
label: string,
value?: number,
customObject?: Record<string, unknown>
): void {
// Preserve existing GA event names by using the action as the dataLayer event.
pushGtmEvent(action, {
event_category: category,
event_label: label,
...(value !== undefined ? { value } : {}),
...(customObject ?? {}),
});
}
+123
View File
@@ -0,0 +1,123 @@
export interface Gtag {
(
command: 'config',
targetId: string,
config?: ConfigParams | ControlParams | EventParams | CustomParams
): void;
(command: 'set', targetId: string, config: CustomParams | boolean): void;
(command: 'set', config: CustomParams): void;
(command: 'js', config: Date): void;
(
command: 'event',
eventName: EventNames | string,
eventParams?: ControlParams | EventParams | CustomParams
): void;
(
command: 'get',
targetId: string,
fieldName: FieldNames | string,
callback?: (field: string) => any
): void;
(
command: 'consent',
consentArg: ConsentArg | string,
consentParams: ConsentParams
): void;
}
interface CustomParams {
[key: string]: any;
}
interface ConfigParams {
page_location?: string;
page_path?: string;
page_title?: string;
send_page_view?: boolean;
}
interface ControlParams {
groups?: string | string[];
send_to?: string | string[];
event_callback?: () => void;
event_timeout?: number;
}
type EventNames =
| 'add_payment_info'
| 'add_to_cart'
| 'add_to_wishlist'
| 'begin_checkout'
| 'checkout_progress'
| 'exception'
| 'generate_lead'
| 'login'
| 'page_view'
| 'purchase'
| 'refund'
| 'remove_from_cart'
| 'screen_view'
| 'search'
| 'select_content'
| 'set_checkout_option'
| 'share'
| 'sign_up'
| 'timing_complete'
| 'view_item'
| 'view_item_list'
| 'view_promotion'
| 'view_search_results';
interface EventParams {
checkout_option?: string;
checkout_step?: number;
content_id?: string;
content_type?: string;
coupon?: string;
currency?: string;
description?: string;
fatal?: boolean;
items?: Item[];
method?: string;
number?: string;
promotions?: Promotion[];
screen_name?: string;
search_term?: string;
shipping?: Currency;
tax?: Currency;
transaction_id?: string;
value?: number;
event_label?: string;
event_category?: string;
}
type Currency = string | number;
interface Item {
brand?: string;
category?: string;
creative_name?: string;
creative_slot?: string;
id?: string;
location_id?: string;
name?: string;
price?: Currency;
quantity?: number;
}
interface Promotion {
creative_name?: string;
creative_slot?: string;
id?: string;
name?: string;
}
type FieldNames = 'client_id' | 'session_id' | 'gclid';
type ConsentArg = 'default' | 'update';
interface ConsentParams {
ad_storage?: 'granted' | 'denied';
analytics_storage?: 'granted' | 'denied';
wait_for_update?: number;
region?: string[];
}
@@ -0,0 +1,71 @@
'use client';
import { useEffect, useRef, useCallback } from 'react';
import { usePathname } from 'next/navigation';
import { sendCustomEventViaGtm } from './google-analytics';
const SCROLL_THRESHOLDS = [10, 25, 50, 75, 90] as const;
export function useWindowScrollDepth(): void {
const pathname = usePathname();
const firedThresholds = useRef<Set<number>>(new Set());
const shouldTrackScroll = useRef(true);
const rafId = useRef<number | null>(null);
const handleScroll = useCallback(() => {
if (!shouldTrackScroll.current) return;
if (typeof window === 'undefined') return;
const scrollPercentage =
((window.scrollY + window.innerHeight) /
document.documentElement.scrollHeight) *
100;
// Fire events for all thresholds we've passed but haven't fired yet
for (const threshold of SCROLL_THRESHOLDS) {
if (
scrollPercentage >= threshold &&
!firedThresholds.current.has(threshold)
) {
firedThresholds.current.add(threshold);
sendCustomEventViaGtm(`scroll_${threshold}`, 'scroll', pathname || '/');
}
}
}, [pathname]);
const throttledHandleScroll = useCallback(() => {
if (rafId.current !== null) return;
rafId.current = requestAnimationFrame(() => {
handleScroll();
rafId.current = null;
});
}, [handleScroll]);
useEffect(() => {
shouldTrackScroll.current = false;
const timeout = setTimeout(() => {
firedThresholds.current = new Set();
shouldTrackScroll.current = true;
// Immediately check current scroll position to capture thresholds
// that may have been passed during the delay
handleScroll();
}, 500);
return () => clearTimeout(timeout);
}, [pathname, handleScroll]);
useEffect(() => {
if (typeof window === 'undefined') return;
window.addEventListener('scroll', throttledHandleScroll, { passive: true });
return () => {
window.removeEventListener('scroll', throttledHandleScroll);
if (rafId.current !== null) {
cancelAnimationFrame(rafId.current);
}
};
}, [throttledHandleScroll]);
}
+20
View File
@@ -0,0 +1,20 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"jsx": "react-jsx",
"allowJs": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
}
]
}
@@ -0,0 +1,21 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"types": ["node"],
"composite": true,
"declaration": true
},
"files": [
"../../node_modules/@nx/react/typings/cssmodule.d.ts",
"../../node_modules/@nx/react/typings/image.d.ts"
],
"exclude": [
"**/*.spec.ts",
"**/*.test.ts",
"**/*.spec.tsx",
"**/*.test.tsx",
"jest.config.ts"
],
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
}
@@ -0,0 +1,19 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "dist/spec",
"types": ["jest", "node"]
},
"include": [
"**/*.spec.ts",
"**/*.test.ts",
"**/*.spec.tsx",
"**/*.test.tsx",
"**/*.spec.js",
"**/*.test.js",
"**/*.spec.jsx",
"**/*.test.jsx",
"**/*.d.ts",
"jest.config.ts"
]
}
@@ -0,0 +1,4 @@
import { baseConfig, reactHooksV7Off } from '../../eslint.config.mjs';
import nx from '@nx/eslint-plugin';
export default [...baseConfig, ...nx.configs['flat/react'], ...reactHooksV7Off];
+12
View File
@@ -0,0 +1,12 @@
{
"presets": [
[
"@nx/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}
+7
View File
@@ -0,0 +1,7 @@
# nx-dev-feature-search
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test nx-dev-feature-search` to execute the unit tests via [Jest](https://jestjs.io).
+4
View File
@@ -0,0 +1,4 @@
import { baseConfig, reactHooksV7Off } from '../../eslint.config.mjs';
import nx from '@nx/eslint-plugin';
export default [...baseConfig, ...nx.configs['flat/react'], ...reactHooksV7Off];
+14
View File
@@ -0,0 +1,14 @@
module.exports = {
displayName: 'nx-dev-feature-search',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/nx-dev/feature-search',
preset: '../../jest.preset.js',
moduleNameMapper: {
// Override for nx-dev packages - point to packages directory
'^@nx/devkit$': '<rootDir>/../../packages/devkit/index.ts',
'^@nx/devkit/testing$': '<rootDir>/../../packages/devkit/testing.ts',
'^@nx/devkit/internal-testing-utils$':
'<rootDir>/../../packages/devkit/internal-testing-utils.ts',
'^@nx/devkit/src/(.*)$': '<rootDir>/../../packages/devkit/src/$1',
},
};
+8
View File
@@ -0,0 +1,8 @@
{
"name": "@nx/nx-dev-feature-search",
"version": "0.0.1",
"type": "commonjs",
"private": true,
"main": "./src/index.ts",
"types": "./src/index.ts"
}
+8
View File
@@ -0,0 +1,8 @@
{
"name": "nx-dev-feature-search",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "nx-dev/feature-search/src",
"projectType": "library",
"targets": {},
"tags": ["scope:nx-dev", "type:feature"]
}
+1
View File
@@ -0,0 +1 @@
export * from './lib/algolia-search';
@@ -0,0 +1,445 @@
.DocSearch--active {
@apply overflow-hidden !important;
}
.DocSearch-VisuallyHiddenForAccessibility {
visibility: hidden;
}
body .DocSearch-Container {
@apply fixed top-0 left-0 z-[50] flex h-screen w-screen cursor-auto flex-col bg-black/10 p-4 backdrop-blur-xs sm:p-6 md:p-[10vh] lg:p-[12vh] dark:bg-white/10;
}
.DocSearch-LoadingIndicator svg {
@apply hidden;
}
.DocSearch-LoadingIndicator {
display: none;
width: 1.5rem;
height: 1.5rem;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none'%3E%3Ccircle cx='12' cy='12' r='9' stroke-width='2' stroke='%2393C5FD' /%3E%3Cpath d='M3,12a9,9 0 1,0 18,0a9,9 0 1,0 -18,0' stroke-width='2' stroke='%233B82F6' stroke-dasharray='56.5486677646' stroke-dashoffset='37.6991118431' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
background-size: 100% 100%;
}
.DocSearch-Container--Stalled .DocSearch-LoadingIndicator {
@apply block;
}
.DocSearch-Modal {
margin: 0 auto;
width: 100%;
max-width: 47.375rem;
display: flex;
flex-direction: column;
min-height: 0;
border-radius: theme('borderRadius.lg');
box-shadow: theme('boxShadow.lg');
background: none;
@apply bg-white dark:bg-slate-900;
}
.DocSearch-SearchBar {
z-index: 1;
padding: 0 1rem;
@apply relative flex flex-none items-center border-b border-slate-200 dark:border-slate-800;
}
.DocSearch-Form {
@apply flex min-w-0 flex-auto items-center;
}
.DocSearch-Dropdown-Container {
@apply pb-6;
}
.DocSearch-Dropdown {
@apply flex-auto overflow-auto;
}
.DocSearch-Hit--Result {
@apply relative mx-6;
}
.DocSearch-Hit--Result.DocSearch-Hit--Child {
@apply ml-12;
}
.DocSearch-Hit--Result.DocSearch-Hit--Child::before {
content: '';
position: absolute;
top: -0.25rem;
bottom: -0.25rem;
left: -1rem;
width: 1px;
@apply bg-slate-200 dark:bg-slate-700;
}
.DocSearch-Hit > a {
position: relative;
font-size: 0.875rem;
@apply border-b border-slate-200 dark:border-slate-800;
}
.DocSearch-Hit--Result {
@apply border-b-0 !important;
}
.DocSearch-MagnifierLabel {
@apply h-6 w-6 flex-none;
/* magnifying glass icon */
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m19 19-3.5-3.5' stroke='%23475569' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3Ccircle cx='11' cy='11' r='6' stroke='%23475569' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
.DocSearch-MagnifierLabel svg {
display: none;
}
.DocSearch-Container--Stalled .DocSearch-MagnifierLabel {
display: none;
}
.DocSearch-Input {
appearance: none;
background: transparent !important;
border: none !important;
box-shadow: none !important;
flex: auto;
font-size: 1rem;
height: 3.5rem;
margin-left: 0.75rem;
margin-right: 1rem;
min-width: 0;
@apply text-slate-700 dark:text-slate-300;
}
@screen sm {
.DocSearch-Input {
font-size: 0.875rem;
}
}
.DocSearch-Input::-webkit-search-cancel-button,
.DocSearch-Input::-webkit-search-decoration,
.DocSearch-Input::-webkit-search-results-button,
.DocSearch-Input::-webkit-search-results-decoration {
display: none;
}
.DocSearch-Reset {
display: none;
}
.DocSearch-Cancel {
appearance: none;
flex: none;
font-size: 0;
border-radius: 0.375rem;
padding: 0.25rem 0.375rem;
@apply border border-slate-900/5 hover:border-slate-900/10 dark:border-slate-200/10 dark:hover:border-slate-200/20;
width: 1.75rem;
height: 1.5rem;
/* esc icon */
background-image: url("data:image/svg+xml,%3Csvg width='16' height='7' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M.506 6h3.931V4.986H1.736v-1.39h2.488V2.583H1.736V1.196h2.69V.182H.506V6ZM8.56 1.855h1.18C9.721.818 8.87.102 7.574.102c-1.276 0-2.21.705-2.205 1.762-.003.858.602 1.35 1.585 1.585l.634.159c.633.153.986.335.988.727-.002.426-.406.716-1.03.716-.64 0-1.1-.295-1.14-.878h-1.19c.03 1.259.931 1.91 2.343 1.91 1.42 0 2.256-.68 2.259-1.745-.003-.969-.733-1.483-1.744-1.71l-.523-.125c-.506-.117-.93-.304-.92-.722 0-.375.332-.65.934-.65.588 0 .949.267.994.724ZM15.78 2.219C15.618.875 14.6.102 13.254.102c-1.537 0-2.71 1.086-2.71 2.989 0 1.898 1.153 2.989 2.71 2.989 1.492 0 2.392-.992 2.526-2.063l-1.244-.006c-.117.623-.606.98-1.262.98-.883 0-1.483-.656-1.483-1.9 0-1.21.591-1.9 1.492-1.9.673 0 1.159.389 1.253 1.028h1.244Z' fill='%236B7280'/%3E%3C/svg%3E") !important;
background-position: center;
background-repeat: no-repeat;
background-size: 57.1428571429% auto;
}
.DocSearch-Reset svg {
display: none;
}
.DocSearch-Hit-source {
line-height: 1.5rem;
font-weight: 600;
padding-top: 2.5rem;
margin: 0 1.5rem 1rem;
@apply text-slate-900 dark:text-slate-300;
}
.DocSearch-Hits:first-child .DocSearch-Hit-source {
padding-top: 1.5rem;
}
.DocSearch-Hit-Container {
display: flex;
align-items: center;
}
.DocSearch-Hit-Tree {
display: none;
}
.DocSearch-Hit-icon {
flex: none;
margin-right: 0.875rem;
}
.DocSearch-Hit--Result .DocSearch-Hit-icon {
display: block;
flex: none;
margin-right: 1rem;
width: 1.5rem;
height: 1.5rem;
border-radius: theme('borderRadius.md');
/* hash icon */
background-image: url("data:image/svg+xml,%3Csvg width='12' height='12' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M3.75 1v10M8.25 1v10M1 3.75h10M1 8.25h10' stroke='%2394A3B8' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: center center;
@apply bg-white shadow-xs ring-1 ring-slate-900/5 dark:ring-slate-100/5;
}
.DocSearch-Hit-content-wrapper {
flex: auto;
display: flex;
flex-direction: column-reverse;
min-width: 0;
z-index: 1;
}
.DocSearch-Hit-path {
align-self: flex-start;
font-size: 0.75rem;
line-height: 1.5rem;
font-weight: 600;
border-radius: 999px;
padding: 0 0.375rem;
@apply border border-slate-200 bg-slate-100 text-slate-700 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-400;
}
.DocSearch-Hit-title {
@apply truncate leading-6 text-slate-900 dark:text-slate-200;
}
.DocSearch-Hit-title + .DocSearch-Hit-path {
@apply mb-1;
}
.DocSearch-Hit-action {
@apply ml-3.5 flex-none;
}
.DocSearch-Hit-action svg {
@apply hidden;
}
.DocSearch-Hit-action {
@apply h-6 w-6;
}
.DocSearch-Hit-action-button {
@apply flex;
}
.DocSearch-Hit-action + .DocSearch-Hit-action {
@apply ml-2;
}
.DocSearch-Hit-action path {
stroke-width: 2px;
stroke: #71717a;
}
.DocSearch-Hit[aria-selected='true'] .DocSearch-Hit-action path {
stroke: white;
}
.DocSearch-Hit > a {
@apply block px-6 py-4;
}
.DocSearch-Hit--Result {
@apply px-4 py-3 !important;
}
.DocSearch-Hit {
@apply relative;
}
.DocSearch-Hit:first-child > a {
@apply border-t border-slate-200 dark:border-slate-800;
}
.DocSearch-Hit--Result {
@apply border-t-0 !important;
}
.DocSearch-Hit + .DocSearch-Hit .DocSearch-Hit--Result {
@apply mt-2;
}
.DocSearch-Hit--Result {
@apply rounded-lg bg-slate-50 dark:bg-slate-800/60;
}
.DocSearch-Hit[aria-selected='true'] > a {
@apply bg-slate-50 dark:bg-slate-800/60;
}
.DocSearch-Hit[aria-selected='true'] .DocSearch-Hit--Result {
@apply bg-slate-50 dark:bg-slate-800/60;
}
.DocSearch-Hit--FirstChild::before {
@apply top-0 !important;
}
.DocSearch-Hit--LastChild::before {
@apply bottom-0 !important;
}
.DocSearch-Hits mark {
background: none;
@apply border-b-2 border-blue-500 font-semibold text-blue-500 dark:border-sky-500 dark:text-sky-500;
}
.DocSearch-Hit-path mark {
@apply border-0;
}
.DocSearch-Footer {
@apply flex flex-none justify-end border-t border-slate-200 px-6 py-4 dark:border-slate-800;
}
.DocSearch-Commands {
@apply hidden;
}
.DocSearch-Logo a {
@apply flex items-center text-xs font-medium text-slate-400 dark:text-slate-600;
}
.DocSearch-Logo svg {
@apply ml-3 text-[#5468ff];
}
.DocSearch-Hit--deleting,
.DocSearch-Hit--favoriting {
opacity: 0;
transition: all 250ms linear;
}
.DocSearch-NoResults .DocSearch-Screen-Icon {
@apply hidden;
}
.DocSearch-Title {
@apply mb-10 text-lg leading-6;
}
.DocSearch-Title strong {
@apply font-normal text-slate-900 dark:text-slate-200;
}
.DocSearch-StartScreen .DocSearch-Help {
@apply px-6 py-16 text-center text-slate-400 dark:text-slate-600;
}
.DocSearch-NoResults {
@apply px-4 pt-10 pb-8;
}
.DocSearch-NoResults .DocSearch-Title {
text-align: center;
@apply text-slate-500;
}
.DocSearch-NoResults-Prefill-List .DocSearch-Help {
@apply mb-3 text-sm leading-6 font-semibold text-slate-900 dark:text-slate-200;
}
.DocSearch-NoResults-Prefill-List ul {
@apply rounded-lg border border-slate-200 bg-slate-50 dark:border-slate-800 dark:bg-slate-900;
}
.DocSearch-NoResults-Prefill-List button {
padding: 0.5rem 0.75rem;
display: block;
width: 100%;
text-align: left;
font-size: theme('fontSize.sm');
line-height: theme('lineHeight.6');
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m11 9 3 3-3 3' stroke='%23475569' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 0.75rem center;
@apply text-slate-700 dark:text-slate-400;
}
.DocSearch-NoResults-Prefill-List li + li button {
@apply border-t border-slate-200 dark:border-slate-800;
}
.DocSearch-NoResults-Prefill-List + .DocSearch-Help {
@apply mt-3 text-xs leading-6;
}
.DocSearch-NoResults-Prefill-List + .DocSearch-Help a {
@apply font-semibold text-blue-500 dark:text-sky-500;
}
.DocSearch-Hit-action [title='Save this search'],
.DocSearch-Hit-action [title='Remove this search from history'],
.DocSearch-Hit-action [title='Remove this search from favorites'] {
width: 1.5rem;
height: 1.5rem;
}
.DocSearch-Hit-action [title='Save this search'] svg,
.DocSearch-Hit-action [title='Remove this search from history'] svg,
.DocSearch-Hit-action [title='Remove this search from favorites'] svg {
display: none;
}
.DocSearch-Hit-action [title='Save this search'] {
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m12 5 2 5h5l-4 4 2 5-5-3-5 3 2-5-4-4h5l2-5Z' stroke='%23CBD5E1' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
.DocSearch-Hit-action [title='Save this search']:hover {
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m12 5 2 5h5l-4 4 2 5-5-3-5 3 2-5-4-4h5l2-5Z' stroke='%23475569' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
.DocSearch-Hit-action [title='Remove this search from history'] {
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M17 7 7 17M7 7l10 10' stroke='%2394A3B8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
.DocSearch-Hit-action [title='Remove this search from history']:hover {
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M17 7 7 17M7 7l10 10' stroke='%23475569' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
.DocSearch-Hit-action [title='Remove this search from favorites']::before {
@apply pointer-events-none absolute inset-0 bg-slate-50 content-[''];
}
.DocSearch-Hit[aria-selected='true']
[title='Remove this search from favorites']::before {
@apply bg-slate-100 dark:bg-slate-800;
}
.DocSearch-Hit-action [title='Remove this search from favorites']::after {
content: '';
position: absolute;
pointer-events: none;
width: calc(4.5rem + 1px);
height: 1.5rem;
margin-left: calc(-3rem - 1px);
background-image:
url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m12 5 2 5h5l-4 4 2 5-5-3-5 3 2-5-4-4h5l2-5Z' fill='%23143157' stroke='%23143157' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"),
url("data:image/svg+xml,%3Csvg width='1' height='1' fill='%23e2e8f0' xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='1' height='1'/%3E%3C/svg%3E"),
url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M17 7 7 17M7 7l10 10' stroke='%2394A3B8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
background-repeat: no-repeat, repeat-y, no-repeat;
background-position: left, center, right;
}
.DocSearch-Hit-action [title='Remove this search from favorites']:hover::after {
background-image:
url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m12 5 2 5h5l-4 4 2 5-5-3-5 3 2-5-4-4h5l2-5Z' fill='%23143157' stroke='%23143157' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"),
url("data:image/svg+xml,%3Csvg width='1' height='1' fill='%23e2e8f0' xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='1' height='1'/%3E%3C/svg%3E"),
url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M17 7 7 17M7 7l10 10' stroke='%23475569' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
.DocSearch-Hit-action + .DocSearch-Hit-action {
@apply ml-3 border-l border-slate-200 pl-3 dark:border-slate-800;
}
@@ -0,0 +1,189 @@
import * as docsearchReact from '@docsearch/react';
// Note: InternalDocSearchHit and StoredDocSearchHit are not exported by @docsearch/react
// import type {
// InternalDocSearchHit,
// StoredDocSearchHit,
// } from '@docsearch/react';
import { MagnifyingGlassIcon } from '@heroicons/react/24/solid';
import Head from 'next/head';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { ReactNode, useCallback, useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
const { DocSearchModal, useDocSearchKeyboardEvents } = docsearchReact;
const ACTION_KEY_DEFAULT = ['Ctrl ', 'Control'];
const ACTION_KEY_APPLE = ['⌘', 'Command'];
function Hit({
hit,
children,
}: {
hit: any; // TODO: Import proper types when @docsearch/react exports them
children: ReactNode;
}): JSX.Element {
return (
<Link href={hit.url} prefetch={false}>
{children}
</Link>
);
}
export function AlgoliaSearch({
tiny = false,
blogOnly = false,
}: {
tiny?: boolean;
blogOnly?: boolean;
}): JSX.Element {
const router = useRouter();
const [isOpen, setIsOpen] = useState(false);
const searchButtonRef = useRef<HTMLButtonElement>(null);
const [initialQuery, setInitialQuery] = useState('');
const [browserDetected, setBrowserDetected] = useState(false);
const [actionKey, setActionKey] = useState(ACTION_KEY_DEFAULT);
const handleOpen = useCallback(() => {
setIsOpen(true);
}, [setIsOpen]);
const handleClose = useCallback(() => {
setIsOpen(false);
}, [setIsOpen]);
const handleInput = useCallback(
(event: KeyboardEvent) => {
setIsOpen(true);
setInitialQuery(event.key);
},
[setIsOpen, setInitialQuery]
);
useDocSearchKeyboardEvents({
isOpen,
onOpen: handleOpen,
onClose: handleClose,
onInput: handleInput,
searchButtonRef,
});
useEffect(() => {
if (typeof navigator !== 'undefined') {
if (/(Mac|iPhone|iPod|iPad)/i.test(navigator.platform)) {
setActionKey(ACTION_KEY_APPLE);
} else {
setActionKey(ACTION_KEY_DEFAULT);
}
setBrowserDetected(true);
}
}, []);
return (
<>
<Head>
<link
rel="preconnect"
href="https://PCTGM1JTQL-dsn.algolia.net"
crossOrigin="anonymous"
/>
</Head>
{!tiny ? (
<button
type="button"
ref={searchButtonRef}
onClick={handleOpen}
className="flex w-full items-center rounded-md bg-white px-2 py-1.5 text-sm leading-4 ring-1 ring-zinc-300 transition dark:bg-zinc-700 dark:ring-zinc-900"
>
<MagnifyingGlassIcon className="h-4 w-4 flex-none" />
<span className="mx-3 inline-flex text-xs text-zinc-300 md:text-sm dark:text-zinc-400">
Search
</span>
<span
style={{ opacity: browserDetected ? '1' : '0' }}
className="ml-auto hidden flex-none rounded-md border border-zinc-200 bg-zinc-50 px-1 py-0.5 text-xs font-semibold text-zinc-500 md:block dark:border-zinc-700 dark:bg-zinc-800/60"
>
<span className="sr-only">Press </span>
<kbd className="font-sans">
<abbr title={actionKey[1]} className="no-underline">
{actionKey[0]}
</abbr>
</kbd>
<span className="sr-only"> and </span>
<kbd className="font-sans">K</kbd>
<span className="sr-only"> to search</span>
</span>
</button>
) : (
<button
type="button"
ref={searchButtonRef}
onClick={handleOpen}
className="inline-flex items-center"
>
<span
style={{ opacity: browserDetected ? '1' : '0' }}
className="ml-auto block flex-none rounded-md border border-zinc-200 bg-zinc-50/60 px-1 py-0.5 text-xs font-semibold text-zinc-400 transition hover:text-zinc-500 dark:border-zinc-700 dark:bg-zinc-800/60 dark:text-zinc-500 dark:hover:text-zinc-400"
>
<span className="sr-only">Press </span>
<kbd className="font-sans">
<abbr title={actionKey[1]} className="no-underline">
{actionKey[0]}
</abbr>
</kbd>
<span className="sr-only"> and </span>
<kbd className="font-sans">K</kbd>
<span className="sr-only"> to search</span>
</span>
</button>
)}
{isOpen &&
createPortal(
<DocSearchModal
searchParameters={{
facetFilters: blogOnly
? ['language:en', 'hierarchy.lvl0:Nx | Blog']
: ['language:en'],
hitsPerPage: 100,
distinct: true,
}}
initialQuery={initialQuery}
placeholder={blogOnly ? 'Search blog posts' : 'Search the docs'}
initialScrollY={window.scrollY}
onClose={handleClose}
indexName={`${process.env.NEXT_PUBLIC_SEARCH_INDEX}`}
apiKey={`${process.env.NEXT_PUBLIC_SEARCH_API_KEY}`}
appId={`${process.env.NEXT_PUBLIC_SEARCH_APP_ID}`}
navigator={{
navigate({ itemUrl }) {
setIsOpen(false);
router.push(itemUrl);
},
}}
insights={true}
hitComponent={Hit}
transformItems={(items) => {
return items.map((item, index) => {
const a = document.createElement('a');
a.href = item.url;
const hash = a.hash === '#content-wrapper' ? '' : a.hash;
if (item.hierarchy?.lvl0) {
item.hierarchy.lvl0 = item.hierarchy.lvl0.replace(
/&amp;/g,
'&'
);
}
return {
...item,
url: `${a.pathname}${hash}`,
};
});
}}
/>,
document.body
)}
</>
);
}
+20
View File
@@ -0,0 +1,20 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"jsx": "react-jsx",
"allowJs": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
}
]
}
+22
View File
@@ -0,0 +1,22 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"types": ["node"],
"lib": ["dom", "es2021"],
"composite": true,
"declaration": true
},
"files": [
"../../node_modules/@nx/react/typings/cssmodule.d.ts",
"../../node_modules/@nx/react/typings/image.d.ts"
],
"exclude": [
"**/*.spec.ts",
"**/*.test.ts",
"**/*.spec.tsx",
"**/*.test.tsx",
"jest.config.ts"
],
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
}
+19
View File
@@ -0,0 +1,19 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "dist/spec",
"types": ["jest", "node"]
},
"include": [
"**/*.spec.ts",
"**/*.test.ts",
"**/*.spec.tsx",
"**/*.test.tsx",
"**/*.spec.js",
"**/*.test.js",
"**/*.spec.jsx",
"**/*.test.jsx",
"**/*.d.ts",
"jest.config.ts"
]
}
+10
View File
@@ -0,0 +1,10 @@
{
"presets": [
[
"@nx/js/babel",
{
"useBuiltIns": "usage"
}
]
]
}
+7
View File
@@ -0,0 +1,7 @@
# nx-dev-models-document
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test nx-dev-models-document` to execute the unit tests via [Jest](https://jestjs.io).
+3
View File
@@ -0,0 +1,3 @@
import { baseConfig } from '../../eslint.config.mjs';
export default [...baseConfig];
+16
View File
@@ -0,0 +1,16 @@
/* eslint-disable */
module.exports = {
displayName: 'nx-dev-models-document',
globals: {},
transform: {
'^.+\\.[tj]sx?$': [
'ts-jest',
{
tsconfig: '<rootDir>/tsconfig.spec.json',
},
],
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/nx-dev/models-document',
preset: '../../jest.preset.js',
};
+8
View File
@@ -0,0 +1,8 @@
{
"name": "@nx/nx-dev-models-document",
"version": "0.0.1",
"type": "commonjs",
"private": true,
"main": "./src/index.ts",
"types": "./src/index.ts"
}
+8
View File
@@ -0,0 +1,8 @@
{
"name": "nx-dev-models-document",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "nx-dev/models-document/src",
"projectType": "library",
"targets": {},
"tags": ["scope:nx-dev", "type:util"]
}
+4
View File
@@ -0,0 +1,4 @@
export * from './lib/documents.models';
export * from './lib/documents.transformers';
export * from './lib/related-documents.utils';
export { pkgToGeneratedApiDocs } from './lib/mappings';
@@ -0,0 +1,42 @@
/**
* @deprecated
*/
export interface DocumentData {
filePath: string;
data: { [key: string]: any };
content: string;
relatedContent: string;
tags: string[];
}
export interface DocumentMetadata {
id: string;
name: string;
description: string;
mediaImage?: string;
file: string;
path: string;
isExternal: boolean;
itemList: DocumentMetadata[];
tags: string[];
}
export interface ProcessedDocument {
content: string;
description: string;
mediaImage?: string;
filePath: string;
id: string;
name: string;
relatedDocuments: Record<string, RelatedDocument[]>;
parentDocuments?: RelatedDocument[];
tags: string[];
}
export interface RelatedDocument {
description: string;
file: string;
id: string;
name: string;
path: string;
}
@@ -0,0 +1,42 @@
import { DocumentMetadata } from './documents.models';
export function createDocumentMetadata(
defaults: Partial<DocumentMetadata> = {}
): DocumentMetadata {
if (!defaults.id) throw new Error('A document entry requires an "id".');
return Object.assign(
{},
{
id: 'fake-id',
name: '',
description: '',
file: '',
itemList: [],
isExternal: false,
path: '',
tags: [],
},
defaults
);
}
export function convertToDocumentMetadata(
target: Partial<DocumentMetadata>
): DocumentMetadata {
if (!target.id) throw new Error('A document entry requires an "id".');
return {
id: target.id,
name: target.name ?? '',
description: target.description ?? '',
mediaImage: target.mediaImage ?? '',
file: target.file ?? '',
itemList: target.itemList
? target.itemList.map((item) => convertToDocumentMetadata(item))
: [],
isExternal: target.isExternal ?? false,
path: target.path ?? '',
tags: target.tags ?? [],
};
}
+186
View File
@@ -0,0 +1,186 @@
/*
* We present a different grouping or naming for the API docs compared to their package names.
* - pagePath - the path to the package's API docs page
* - includeDocuments - whether to include documents, which should eventually be moved to guides
*
* This mapping is used in:
* - scripts/documentation/generators/generate-manifests.ts (generate menus)
* - nx-dev/data-access-documents/src/lib/documents.api.ts (generate page urls)
* - nx-dev/nx-dev/pages/[...segments].tsx (generate page content)
*/
export const pkgToGeneratedApiDocs: Record<
string,
{ pagePath: string; introPath: string; includeDocuments?: boolean }
> = {
// ts/js
js: {
pagePath: '/technologies/typescript/api',
introPath: '/technologies/typescript/introduction',
},
// angular
angular: {
pagePath: '/technologies/angular/api',
introPath: '/technologies/angular/introduction',
},
// react
react: {
pagePath: '/technologies/react/api',
introPath: '/technologies/react/introduction',
},
'react-native': {
pagePath: '/technologies/react/react-native/api',
introPath: '/technologies/react/react-native/introduction',
},
expo: {
pagePath: '/technologies/react/expo/api',
introPath: '/technologies/react/expo/introduction',
},
next: {
pagePath: '/technologies/react/next/api',
introPath: '/technologies/react/next/introduction',
},
remix: {
pagePath: '/technologies/react/remix/api',
introPath: '/technologies/react/remix/introduction',
},
// vue
vue: {
pagePath: '/technologies/vue/api',
introPath: '/technologies/vue/introduction',
},
nuxt: {
pagePath: '/technologies/vue/nuxt/api',
introPath: '/technologies/vue/nuxt/introduction',
},
// node
node: {
pagePath: '/technologies/node/api',
introPath: '/technologies/node/introduction',
},
express: {
pagePath: '/technologies/node/express/api',
introPath: '/technologies/node/express/introduction',
},
nest: {
pagePath: '/technologies/node/nest/api',
introPath: '/technologies/node/nest/introduction',
},
// java
gradle: {
pagePath: '/technologies/java/api',
introPath: '/technologies/java/introduction',
},
// build tools
webpack: {
pagePath: '/technologies/build-tools/webpack/api',
introPath: '/technologies/build-tools/webpack/introduction',
},
vite: {
pagePath: '/technologies/build-tools/vite/api',
introPath: '/technologies/build-tools/vite/introduction',
},
rollup: {
pagePath: '/technologies/build-tools/rollup/api',
introPath: '/technologies/build-tools/rollup/introduction',
},
esbuild: {
pagePath: '/technologies/build-tools/esbuild/api',
introPath: '/technologies/build-tools/esbuild/introduction',
},
rspack: {
pagePath: '/technologies/build-tools/rspack/api',
introPath: '/technologies/build-tools/rspack/introduction',
},
rsbuild: {
pagePath: '/technologies/build-tools/rsbuild/api',
introPath: '/technologies/build-tools/rsbuild/introduction',
},
// test tools
jest: {
pagePath: '/technologies/test-tools/jest/api',
introPath: '/technologies/test-tools/jest/introduction',
},
storybook: {
pagePath: '/technologies/test-tools/storybook/api',
introPath: '/technologies/test-tools/storybook/introduction',
},
playwright: {
pagePath: '/technologies/test-tools/playwright/api',
introPath: '/technologies/test-tools/playwright/introduction',
},
cypress: {
pagePath: '/technologies/test-tools/cypress/api',
introPath: '/technologies/test-tools/cypress/introduction',
},
detox: {
pagePath: '/technologies/test-tools/detox/api',
introPath: '/technologies/test-tools/detox/introduction',
},
// misc
'module-federation': {
pagePath: '/technologies/module-federation/api',
introPath: '/technologies/module-federation/introduction',
},
eslint: {
pagePath: '/technologies/eslint/api',
introPath: '/technologies/eslint/introduction',
},
'eslint-plugin': {
pagePath: '/technologies/eslint/eslint-plugin/api',
introPath: '/technologies/eslint/eslint-plugin/api',
},
// core and misc
// For now, things that are not in technologies are put here in references/core-api
nx: {
pagePath: '/reference/core-api/nx',
introPath: '/reference/core-api/nx',
// TODO(docs): move these to guides and remove this
includeDocuments: true,
},
workspace: {
pagePath: '/reference/core-api/workspace',
introPath: '/reference/core-api/workspace',
// TODO(docs): move these to guides and remove this
includeDocuments: true,
},
owners: {
pagePath: '/reference/core-api/owners',
introPath: '/reference/core-api/owners',
},
conformance: {
pagePath: '/reference/core-api/conformance',
introPath: '/reference/core-api/conformance',
// TODO(docs): move these to guides and remove this
includeDocuments: true,
},
'azure-cache': {
pagePath: '/reference/core-api/azure-cache',
introPath: '/reference/core-api/azure-cache',
},
'gcs-cache': {
pagePath: '/reference/core-api/gcs-cache',
introPath: '/reference/core-api/gcs-cache',
},
's3-cache': {
pagePath: '/reference/core-api/s3-cache',
introPath: '/reference/core-api/s3-cache',
},
'shared-fs-cache': {
pagePath: '/reference/core-api/shared-fs-cache',
introPath: '/reference/core-api/shared-fs-cache',
},
devkit: {
pagePath: '/reference/core-api/devkit',
introPath: '/reference/core-api/devkit/documents/nx_devkit',
// TODO(docs): move these to guides and remove this
includeDocuments: true,
},
plugin: {
pagePath: '/reference/core-api/plugin',
introPath: '/reference/core-api/plugin',
},
web: {
pagePath: '/reference/core-api/web',
introPath: '/reference/core-api/web',
},
};
@@ -0,0 +1,56 @@
import { RelatedDocument } from './documents.models';
export interface RelatedDocumentsCategory {
id: string;
/**
* Matcher that will be evaluated against a path.
*/
matchers: string[];
name: string;
relatedDocuments: RelatedDocument[];
}
export function categorizeRelatedDocuments(
items: RelatedDocument[]
): RelatedDocumentsCategory[] {
const categories: RelatedDocumentsCategory[] = [
{
id: 'concepts',
name: 'Concepts',
matchers: [
'/concepts/',
'/concepts/module-federation/',
'/concepts/decisions/',
],
relatedDocuments: [],
},
{
id: 'recipes',
name: 'Guides',
matchers: ['/recipes/', '/guides/'],
relatedDocuments: [],
},
{
id: 'reference',
name: 'Reference',
// TODO(caleb): including /technologies/ in the route will duplicate the display of `recipes/guides` so leaving off for now
matchers: ['/workspace/', '/nx-api/', '/core-api/'],
relatedDocuments: [],
},
{
id: 'see-also',
name: 'See also',
matchers: ['/see-also/'],
relatedDocuments: [],
},
];
items?.forEach((i) =>
categories.forEach((c) => {
if (c.matchers.some((m) => i.path.includes(m)))
c.relatedDocuments.push(i);
})
);
return categories.filter((c) => !!c.relatedDocuments.length);
}
+10
View File
@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.base.json",
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
}
]
}
+11
View File
@@ -0,0 +1,11 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"declaration": true,
"types": ["node"],
"composite": true
},
"exclude": ["**/*.spec.ts", "**/*.test.ts", "jest.config.ts"],
"include": ["**/*.ts"]
}
+19
View File
@@ -0,0 +1,19 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "dist/spec",
"types": ["jest", "node"]
},
"include": [
"**/*.test.ts",
"**/*.spec.ts",
"**/*.test.tsx",
"**/*.spec.tsx",
"**/*.test.js",
"**/*.spec.js",
"**/*.test.jsx",
"**/*.spec.jsx",
"**/*.d.ts",
"jest.config.ts"
]
}
+10
View File
@@ -0,0 +1,10 @@
{
"presets": [
[
"@nx/js/babel",
{
"useBuiltIns": "usage"
}
]
]
}
+7
View File
@@ -0,0 +1,7 @@
# nx-dev-models-menu
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test nx-dev-models-menu` to execute the unit tests via [Jest](https://jestjs.io).
+3
View File
@@ -0,0 +1,3 @@
import { baseConfig } from '../../eslint.config.mjs';
export default [...baseConfig];
+16
View File
@@ -0,0 +1,16 @@
/* eslint-disable */
module.exports = {
displayName: 'nx-dev-models-menu',
globals: {},
transform: {
'^.+\\.[tj]sx?$': [
'ts-jest',
{
tsconfig: '<rootDir>/tsconfig.spec.json',
},
],
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/nx-dev/models-menu',
preset: '../../jest.preset.js',
};
+8
View File
@@ -0,0 +1,8 @@
{
"name": "@nx/nx-dev-models-menu",
"version": "0.0.1",
"type": "commonjs",
"private": true,
"main": "./src/index.ts",
"types": "./src/index.ts"
}

Some files were not shown because too many files have changed in this diff Show More