chore: improvements to interactive playground

This commit is contained in:
Ken Snyder
2022-01-24 02:31:13 -08:00
parent 169230ba2a
commit bc95920acf
32 changed files with 1005 additions and 376 deletions

View File

@@ -4,12 +4,27 @@
## Getting Started
```bash
# install npm dependencies (in all repos)
# 1. installs deps for both CLI and Docs
# 2. starts Docs server (in dev mode with HMR), opens in browser
# 3. starts Meilisearch server in Docker
pnpm run start
```
If you've already installed all the deps and want more granular control you can try any of the following script targets:
```bash
# start Docker services (you must have Docker installed)
docker compose up
# bring up documentation / playground site
pnpm run dev
# or
pnpm run up
# stop Docker services
docker compose down
# or
pnpm run down
# turn on watcher mode for both CLI and Docs
pnpm run watch
```
A browser window should now have opened up pointing to `http://localhost:3333`. See you over there.

View File

@@ -1,15 +1,18 @@
services:
scraper:
image: getmeili/docs-scraper:latest
container_name: scraper
command: pipenv run ./docs_scraper config.json -d
depends_on:
- search
environment:
- MEILISEARCH_HOST_URL=localhost:7700
- MEILISEARCH_API_KEY=""
volumes:
- ./scraper:/data.ms
# We are not using the scraper for anything currently but keeping it here
# for now in case we decide to
# scraper:
# image: getmeili/docs-scraper:latest
# container_name: scraper
# command: pipenv run ./docs_scraper config.json -d
# depends_on:
# - search
# environment:
# - MEILISEARCH_HOST_URL=localhost:7700
# - MEILISEARCH_API_KEY=""
# volumes:
# - ./scraper:/data.ms
search:
image: getmeili/meilisearch:latest

View File

@@ -1,3 +1,83 @@
{
"extends": "@antfu"
"extends": ["plugin:vue/vue3-recommended", "prettier"],
"plugins": ["cypress", "@typescript-eslint"],
"parserOptions": {
"parser": "@typescript-eslint/parser"
},
"settings": {
// https://github.com/meteorlxy/eslint-plugin-prettier-vue#eslint-config
"SFCBlocks": {}
},
"rules": {
// "prettier-vue/prettier": [
// "error",
// {
// "printWidth": 120,
// "singleQuote": false,
// "semi": true
// }
// ],
"vue/no-lone-template": "off",
"vue/no-multiple-template-root": "off",
"vue/no-vmodel-argument": "off",
/**
* turn this back on once the false warning for using string literals goes away;
* for now, so long as we express an "emits" clause then TS should keep us honest
* here rather than relying on lint rules.
*/
"vue/require-explicit-emits": "off",
"no-trailing-spaces": ["warn", { "skipBlankLines": true, "ignoreComments": true }],
"no-console": "warn",
"prefer-const": "error",
"semi": "off",
"quotes": ["warn", "double", { "avoidEscape": true, "allowTemplateLiterals": true }],
"no-unused-vars": "off",
"curly": ["warn", "multi-line"],
"brace-style": ["error", "1tbs", { "allowSingleLine": true }],
// our use of `-spec` files for testing prevents us using this
"unicorn/filename-case": "off",
// reduce has been getting a bad rap lately; its true that often
// a filter or map would be clearer and equally as effective but
// there are still some legit cases to use reduce
"unicorn/no-array-reduce": "off",
"unicorn/prevent-abbreviations": "off",
"unicorn/no-null": "off",
"no-nested-ternary": "off",
// doesn't play well with prettier
"unicorn/no-nested-ternary": "off",
// this is kind of nice sometimes
"unicorn/no-array-callback-reference": "off",
// we need exceptions to be only "warn" because
// there are valid use cases for generic variables being
// used before being defined
"no-use-before-define": ["warn"],
"@typescript-eslint/semi": ["error"],
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/member-delimiter-style": [
"error",
{
"multiline": {
"delimiter": "semi",
"requireLast": true
},
"singleline": {
"delimiter": "semi",
"requireLast": false
}
}
],
// "cases" allows for graceful use of that variable
// name in Typescript test cases
"@typescript-eslint/no-unused-vars": [
"error",
{
"varsIgnorePattern": "cases|^_",
"argsIgnorePattern": "^_"
}
]
}
}

View File

@@ -1,6 +1,7 @@
{
"private": true,
"scripts": {
"start": "pnpm install",
"build": "vite-ssg build",
"dev": "vite --port 3333 --open",
"lint": "eslint \"**/*.{vue,ts,js}\"",
@@ -18,6 +19,7 @@
"nprogress": "^0.2.0",
"pinia": "^2.0.9",
"prism-theme-vars": "^0.2.2",
"tauri-search": "link:../",
"ts-morph": "^13.0.3",
"vue": "^3.2.28",
"vue-demi": "^0.12.1",
@@ -26,14 +28,17 @@
},
"devDependencies": {
"@antfu/eslint-config": "^0.16.0",
"@iconify-json/ant-design": "^1.0.2",
"@iconify-json/bx": "^1.0.3",
"@iconify-json/carbon": "^1.0.14",
"@iconify-json/cib": "^1.0.1",
"@iconify-json/fluent": "^1.0.15",
"@iconify-json/ic": "^1.0.8",
"@iconify-json/mdi": "^1.0.12",
"@iconify-json/ph": "^1.0.4",
"@iconify-json/tabler": "^1.0.12",
"@iconify-json/teenyicons": "^1.0.1",
"@iconify-json/vscode-icons": "^1.0.3",
"@intlify/vite-plugin-vue-i18n": "^3.2.1",
"@types/markdown-it-link-attributes": "^3.0.1",
"@types/nprogress": "^0.2.0",

26
docs/pnpm-lock.yaml generated
View File

@@ -2,14 +2,17 @@ lockfileVersion: 5.3
specifiers:
'@antfu/eslint-config': ^0.16.0
'@iconify-json/ant-design': ^1.0.2
'@iconify-json/bx': ^1.0.3
'@iconify-json/carbon': ^1.0.14
'@iconify-json/cib': ^1.0.1
'@iconify-json/fluent': ^1.0.15
'@iconify-json/ic': ^1.0.8
'@iconify-json/mdi': ^1.0.12
'@iconify-json/ph': ^1.0.4
'@iconify-json/tabler': ^1.0.12
'@iconify-json/teenyicons': ^1.0.1
'@iconify-json/vscode-icons': ^1.0.3
'@intlify/vite-plugin-vue-i18n': ^3.2.1
'@types/markdown-it-link-attributes': ^3.0.1
'@types/nprogress': ^0.2.0
@@ -32,6 +35,7 @@ specifiers:
nprogress: ^0.2.0
pinia: ^2.0.9
prism-theme-vars: ^0.2.2
tauri-search: link:../
ts-morph: ^13.0.3
typescript: ^4.5.5
unplugin-auto-import: ^0.5.11
@@ -60,6 +64,7 @@ dependencies:
nprogress: 0.2.0
pinia: 2.0.9_typescript@4.5.5+vue@3.2.28
prism-theme-vars: 0.2.2
tauri-search: link:..
ts-morph: 13.0.3
vue: 3.2.28
vue-demi: 0.12.1_vue@3.2.28
@@ -68,14 +73,17 @@ dependencies:
devDependencies:
'@antfu/eslint-config': 0.16.0_eslint@8.7.0+typescript@4.5.5
'@iconify-json/ant-design': 1.0.2
'@iconify-json/bx': 1.0.3
'@iconify-json/carbon': 1.0.14
'@iconify-json/cib': 1.0.1
'@iconify-json/fluent': 1.0.15
'@iconify-json/ic': 1.0.8
'@iconify-json/mdi': 1.0.12
'@iconify-json/ph': 1.0.4
'@iconify-json/tabler': 1.0.12
'@iconify-json/teenyicons': 1.0.1
'@iconify-json/vscode-icons': 1.0.3
'@intlify/vite-plugin-vue-i18n': 3.2.1_vite@2.7.13+vue-i18n@9.1.9
'@types/markdown-it-link-attributes': 3.0.1
'@types/nprogress': 0.2.0
@@ -1584,6 +1592,12 @@ packages:
resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==}
dev: true
/@iconify-json/ant-design/1.0.2:
resolution: {integrity: sha512-pyD1RM5gB61vgP1g3F9yMfmZkxCHW6zbHsJJJaBQG1YkJBUmN/9i3pJjyprwCmlOcw16E8DMUWKMCWG00edDCw==}
dependencies:
'@iconify/types': 1.0.12
dev: true
/@iconify-json/bx/1.0.3:
resolution: {integrity: sha512-nwUxwOwocTp5u+KcBUraqEXiC7VG6niL6RQIdbLsRjZwouxayyVXPIkBPwMEmxpcTk1SA8Jh52MI+Scex1wJSA==}
dependencies:
@@ -1620,6 +1634,12 @@ packages:
'@iconify/types': 1.0.12
dev: true
/@iconify-json/ph/1.0.4:
resolution: {integrity: sha512-hcxC2k25/Lh/bgXgbwAD4WvnC8BeunSqafFwIOyL1dCu3QGBgKmPFIBUv4W2kBm+rbrv7F3WHPFBAJDVrjpunA==}
dependencies:
'@iconify/types': 1.0.12
dev: true
/@iconify-json/tabler/1.0.12:
resolution: {integrity: sha512-ZUXFwecBxSrJhtMiJgLwd+ol2etNRl5T/yMfRFRq5ZGIDSmkk+W0ZbkMroSlglo2Eu3UZ4gNYLNg3x3efGzDlg==}
dependencies:
@@ -1632,6 +1652,12 @@ packages:
'@iconify/types': 1.0.12
dev: true
/@iconify-json/vscode-icons/1.0.3:
resolution: {integrity: sha512-3GW+zXEqQxqYCUcglAITagE945qCJUOihCC7yJwE7yPKkYOEsEb+w6GKNkqJ8RMM0vJFA4uNqSJTtqaFS9QF+A==}
dependencies:
'@iconify/types': 1.0.12
dev: true
/@iconify/types/1.0.12:
resolution: {integrity: sha512-6er6wSGF3hgc1JEZqiGpg21CTCjHBYOUwqLmb2Idzkjiw6ogalGP0ZMLVutCzah+0WB4yP+Zd2oVPN8jvJ+Ftg==}
dev: true

View File

@@ -4,22 +4,33 @@
declare module 'vue' {
export interface GlobalComponents {
'AntDesign:fileMarkdownOutlined': typeof import('~icons/ant-design/file-markdown-outlined')['default']
'Bx:bxSearchAlt': typeof import('~icons/bx/bx-search-alt')['default']
'Carbon:document': typeof import('~icons/carbon/document')['default']
'Carbon:errorFilled': typeof import('~icons/carbon/error-filled')['default']
'Carbon:listDropdown': typeof import('~icons/carbon/list-dropdown')['default']
'Carbon:nextFilled': typeof import('~icons/carbon/next-filled')['default']
CarbonLanguage: typeof import('~icons/carbon/language')['default']
CarbonMoon: typeof import('~icons/carbon/moon')['default']
CarbonSun: typeof import('~icons/carbon/sun')['default']
CarbonWarning: typeof import('~icons/carbon/warning')['default']
Counter: typeof import('./components/Counter.vue')['default']
'Fluent:databaseSearch24Regular': typeof import('~icons/fluent/database-search24-regular')['default']
Footer: typeof import('./components/Footer.vue')['default']
'Ic:roundScreenSearchDesktop': typeof import('~icons/ic/round-screen-search-desktop')['default']
'Mdi:folderHome': typeof import('~icons/mdi/folder-home')['default']
'Mdi:github': typeof import('~icons/mdi/github')['default']
'Mdi:languageRust': typeof import('~icons/mdi/language-rust')['default']
'Mdi:languageTypescript': typeof import('~icons/mdi/language-typescript')['default']
'Ph:linkLight': typeof import('~icons/ph/link-light')['default']
README: typeof import('./components/README.md')['default']
SearchActions: typeof import('./components/SearchActions.vue')['default']
SearchHit: typeof import('./components/SearchHit.vue')['default']
SearchResults: typeof import('./components/SearchResults.vue')['default']
SearchStats: typeof import('./components/SearchStats.vue')['default']
SimpleCard: typeof import('./components/SimpleCard.vue')['default']
'Tabler:databaseImport': typeof import('~icons/tabler/database-import')['default']
'Teenyicons:dockerOutline': typeof import('~icons/teenyicons/docker-outline')['default']
'VscodeIcons:fileTypeRust': typeof import('~icons/vscode-icons/file-type-rust')['default']
'VscodeIcons:fileTypeTypescriptOfficial': typeof import('~icons/vscode-icons/file-type-typescript-official')['default']
}
}

View File

@@ -1,61 +1,111 @@
<script setup lang="ts">
import { isDark, toggleDark } from '~/composables'
import { isDark, toggleDark } from "~/composables";
const { t, availableLocales, locale } = useI18n()
const { t, availableLocales, locale } = useI18n();
const toggleLocales = () => {
// change to some real logic
const locales = availableLocales
locale.value = locales[(locales.indexOf(locale.value) + 1) % locales.length]
}
const locales = availableLocales;
locale.value = locales[(locales.indexOf(locale.value) + 1) % locales.length];
};
</script>
<template>
<nav class="text-xl my-2 flex flex-row items-center justify-center space-x-2">
<router-link class=" icon-btn " to="/" :title="t('button.home')">
<mdi:folder-home />
</router-link>
<v-tooltip :delay="{ show: 500, hide: 100 }">
<router-link class=" icon-btn " to="/" :title="t('button.home')">
<mdi:folder-home />
</router-link>
<template #popper>
Docs Home
</template>
</v-tooltip>
<router-link class=" icon-btn " to="/search" :title="t('button.home')">
<bx:bx-search-alt />
</router-link>
<v-tooltip :delay="{ show: 500, hide: 100 }">
<router-link class=" icon-btn " to="/search" :title="t('button.home')">
<bx:bx-search-alt />
</router-link>
<template #popper>
Interactive Playground
</template>
</v-tooltip>
<div class="px-4 text-gray-200/50 dark:text-gray-300/50 text-light ">
|
</div>
<router-link class=" icon-btn " to="/meilisearch" :title="t('nav.indexing')">
<fluent:database-search-24-regular />
</router-link>
<v-tooltip :delay="{ show: 500, hide: 100 }">
<router-link class=" icon-btn " to="/meilisearch" :title="t('nav.indexing')">
<fluent:database-search-24-regular />
</router-link>
<template #popper>
MeiliSearch Info
</template>
</v-tooltip>
<router-link class=" icon-btn " to="/scraper" :title="t('nav.indexing')">
<tabler:database-import />
</router-link>
<v-tooltip :delay="{ show: 500, hide: 100 }">
<router-link class=" icon-btn " to="/scraper" :title="t('nav.indexing')">
<tabler:database-import />
</router-link>
<template #popper>
Scraper
</template>
</v-tooltip>
<router-link class=" icon-btn " to="/docs-searchbar" :title="t('nav.indexing')">
<ic:round-screen-search-desktop />
</router-link>
<v-tooltip :delay="{ show: 500, hide: 100 }">
<router-link class=" icon-btn " to="/docs-searchbar" :title="t('nav.indexing')">
<carbon:list-dropdown />
</router-link>
<template #popper>
Search Bar
</template>
</v-tooltip>
<router-link class=" icon-btn " to="/docker" :title="t('button.about')">
<teenyicons:docker-outline />
</router-link>
<v-tooltip :delay="{ show: 500, hide: 100 }">
<router-link class=" icon-btn " to="/docker" :title="t('button.about')">
<teenyicons:docker-outline />
</router-link>
<template #popper>
local Docker search server
</template>
</v-tooltip>
<div class="px-4 text-gray-200/50 dark:text-gray-300/50 text-light ">
|
</div>
<router-link class="icon-btn " to="/ts-docs">
<mdi:language-typescript />
</router-link>
<router-link class="icon-btn " to="/rust-docs">
<mdi:language-rust />
</router-link>
<v-tooltip :delay="{ show: 500, hide: 100 }">
<router-link class="icon-btn " to="/ts-docs">
<mdi:language-typescript />
</router-link>
<template #popper>
Typescript API Documentation
</template>
</v-tooltip>
<v-tooltip :delay="{ show: 500, hide: 100 }">
<router-link class="icon-btn " to="/rust-docs">
<mdi:language-rust />
</router-link>
<template #popper>
Rust API Documentation
</template>
</v-tooltip>
<v-tooltip :delay="{ show: 500, hide: 100 }">
<router-link class="icon-btn " to="/rust-docs">
<carbon:document />
</router-link>
<template #popper>
Markdown API Documentation
</template>
</v-tooltip>
<div class="px-4 text-gray-200/50 dark:text-gray-300/50 text-light ">
|
</div>
<v-tooltip>
<v-tooltip :delay="{ show: 500, hide: 100 }">
<button class=" icon-btn !outline-none" :title="t('nav.toggle_dark')" @click="toggleDark()">
<carbon-moon v-if="isDark" />
<carbon-sun v-else />
@@ -65,9 +115,14 @@ const toggleLocales = () => {
</template>
</v-tooltip>
<a class=" icon-btn mx-2" :title="t('nav.toggle_langs')" @click="toggleLocales">
<carbon-language />
</a>
<v-tooltip :delay="{ show: 500, hide: 100 }">
<a class=" icon-btn mx-2" :title="t('nav.toggle_langs')" @click="toggleLocales">
<carbon-language />
</a>
<template #popper>
Switch Language for Heading Content
</template>
</v-tooltip>
</nav>
</template>

View File

@@ -0,0 +1,38 @@
<script setup lang="ts">
import { useSearch } from "~/modules/search";
const s = useSearch();
const indexPossibilites = ["repo", "prose", "TS API", "RS API"];
</script>
<template>
<div class="flex flex-col rounded-md bg-gray-100/25 dark:bg-gray-900/25">
<div class="flex flex-col space-y-2 rounded-t-md overflow-hidden">
<div class="font-bold text-lg px-4 py-3 bg-gray-900/10 dark:bg-gray-100/10">
ACTIONS
</div>
<div class="p-4 flex flex-col space-y-2">
<div v-for="idx in indexPossibilites" :key="idx" class=" rounded cursor-pointer p-1 flex">
<div class="actions flex flex-row space-x-1 items-center text-gray-100 dark:text-gray-700 transition-colors duration-200">
<carbon:next-filled class="flex hover:text-green-500" />
<carbon:error-filled class="flex hover:text-orange-500" />
</div>
<div class="flex flex-row ml-2">
<span class="font-medium">{{idx}}&nbsp;</span>
<span class="font-light">pipeline</span>
</div>
</div>
</div>
</div>
</div>
</template>
<style lang="css" scoped>
.btn {
@apply text-xs ;
}
</style>

View File

@@ -0,0 +1,66 @@
<script setup lang="ts">
import { PropType } from "vue";
import { GenericDoc } from "~/types/meilisearch";
const props = defineProps({
document: {
type: Object as PropType<GenericDoc>, required: true}
});
const doc = computed(() => props.document);
const showDetails = ref(false);
const apiKind = computed(() => {
if(doc.value._idx !== "api") {
return "";
}
switch(doc.value.kind) {
case "Function":
return "fn";
case "Namespace":
return "module";
case "Interface":
return "interface";
default:
return doc.value?.kind ? String(doc.value?.kind).toLowerCase() : "";
}
});
const details = () => {
showDetails.value = !showDetails.value;
};
</script>
<template>
<div class="flex flex-col border-1 rounded px-2 py-1 border-gray-500 hover:bg-gray-100 dark:hover:bg-gray-700 ">
<div class="flex flex-row space-x-1 items-center place-content-center cursor-pointer" @click="details">
<mdi:github v-if="doc._idx === 'repo'" class="flex flex-shrink-0" />
<vscode-icons:file-type-typescript-official v-if="doc._idx === 'api' && doc.language === 'typescript'" class="flex flex-shrink-0" />
<vscode-icons:file-type-rust v-if="doc._idx === 'api' && doc.language === 'rust'" class="flex" />
<ant-design:file-markdown-outlined v-if="doc._idx === 'prose'" class="flex" />
<div class="flex flex-shrink-0">{{doc.name}}</div>
<div class="flex flex-grow ml-1 font-light text-xs italic truncate">
{{doc.description}}
</div>
<div v-if="doc._idx === 'api'" class="flex text-xs font-medium px-1 py-0.5 bg-blue-500 dark:bg-blue-600 text-gray-50 rounded">
{{apiKind}}
</div>
<div class="flex px-1 hover:text-green-600 hover:font-bold ">
<a :href="doc.url" target="_new">
<ph:link-light class="flex" />
</a>
</div>
</div>
<div v-if="showDetails" class="items-start">
<div v-for="key in Object.keys(doc)" :key="key" class="flex flex-row">
<span class="flex font-bold">{{key}}:</span>
<span class="flex ml-1">{{doc[key]}}</span>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,30 @@
<script setup lang="ts">
const props = defineProps({
query: {type: String, default: ""}
});
import { useSearch } from "~/modules/search";
const s = useSearch();
</script>
<template>
<div class="search-stats flex flex-col rounded-md h-full">
<div class="rounded-t-md overflow-hidden">
<div class="font-bold text-lg rounded-t-md bg-gray-900/10 dark:bg-gray-100/10 py-3 px-4">
Results
</div>
</div>
<div v-if="s.searchResults.length >0 && s.searchStatus ==='ready'" class="p-4 mb-2 space-y-2">
<search-hit v-for="hit in s.searchResults" :key="hit.id" :document="hit" />
</div>
<div v-else class="flex flex-col flex-grow justify-center italic">
<div v-if="query.length > 0" >
no results
</div>
<div v-else>
add a search query above
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,42 @@
<script setup lang="ts">
import { useSearch } from "~/modules/search";
const s = useSearch();
</script>
<template>
<div class="search-stats flex flex-col rounded-md bg-gray-100/25 dark:bg-gray-800/25">
<div class="rounded-t-md overflow-hidden">
<div class="font-bold text-lg rounded-t-md bg-gray-900/10 dark:bg-gray-100/10 py-3 px-4">
INDEXES
</div>
</div>
<div v-if="s.$state.health" class="text-xs mt-2" >
<div class="text-green-500">MeiliSearch Service Healthy!</div>
<div class="db-size">db size: {{s.dbSize}}</div>
</div>
<span v-else class="text-red-500 ">
Meilisearch is not available locally!
</span>
<div class="p-4">
<div
v-for="idx in s.$state.indexes"
:key="idx.uid"
class="index flex flex-row w-full py-2 border-1 rounded border-gray-500 p-4 mt-4"
>
<span class="index-name flex flex-grow font-medium">{{idx.name}}</span>
<v-tooltip >
<span class="index-name font-light cursor-pointer">
{{s.indexInfo(idx.name)?.numberOfDocuments}}
</span>
<template #popper>
<span class="dark:text-gray-300">
# of Docs
</span>
</template>
</v-tooltip>
</div>
</div>
</div>
</template>

View File

@@ -1,6 +1,13 @@
<script setup lang="ts">
const props = defineProps({
title: { type: String, required: true },
kind: { type: String, required: true },
/** the link URL */
url: { type: String, required: true },
/** prose description */
description: { type: String, required: false },
/** any other properties that don't fit above */
rest: { type: Object, required: false },
})
</script>

178
docs/src/modules/search.ts Normal file
View File

@@ -0,0 +1,178 @@
/* eslint-disable no-console */
/* eslint-disable no-use-before-define */
import { acceptHMRUpdate, defineStore } from "pinia";
import type { UserModule } from "~/types";
import { MeiliSearchHealth, MeiliSearchIndex, MeiliSearchInterface, MeiliSearchResponse, MeiliSearchStats } from "~/types/meilisearch";
//#region STORE
export interface SearchState {
/** is the HTTP server of MeiliSearch reachable */
health: boolean | "initializing";
/** the indexes which are defined on MeiliSearch */
indexes: MeiliSearchInterface[];
/** database stats for MeiliSearch */
stats?: MeiliSearchStats;
searchStatus: "ready" | "searching" | "error" | "not-ready";
searchResults: {id: string; _idx: string; [key: string]: unknown}[];
}
export const useSearch = defineStore("search", ({
state: () => ({
health: "initializing",
indexes: [],
stats: undefined,
searchStatus: "not-ready",
searchResults: [],
}) as SearchState,
actions: {
async search(text: string, indexes?: string[]) {
if(text.trim()==="") {
this.$state.searchResults = [];
return;
}
if(!indexes) {
indexes = this.$state.indexes.map(i => i.uid);
}
console.group(`searching for: "${text}"`);
console.info(`search on ${indexes.length} indexes`, indexes);
console.time("search");
this.$state.searchStatus = "searching";
const waitFor: Promise<any>[] = [];
for (const idx of indexes) {
const addIndex =
(result: MeiliSearchResponse): MeiliSearchResponse => ({
...result,
hits: result.hits.map(i =>
({...i, _idx: idx})
)
});
waitFor.push(
get<
MeiliSearchResponse,
MeiliSearchResponse
>
(api().search(idx,text), addIndex)
);
}
const results = await Promise.all(waitFor);
this.$state.searchStatus = "ready";
console.timeEnd("search");
const hits = results.flatMap(i => i.hits);
console.info(`found ${hits.length} documents`);
console.groupEnd();
this.$state.searchResults = hits;
return results;
},
statsUpdate(s: MeiliSearchStats) {
this.$state.stats = s;
},
healthUpdate(h: boolean) {
this.$state.health = h;
},
indexUpdate(idx: MeiliSearchInterface[]) {
this.$state.indexes = idx;
}
},
getters: {
dbSize: (state) => state.stats?.databaseSize || 0,
indexInfo: (state) => (idx: string): MeiliSearchIndex | undefined => state.stats?.indexes[idx]
}
}));
if (import.meta.hot) {
import.meta.hot.accept(
acceptHMRUpdate(
useSearch,
import.meta.hot)
);
}
//#endregion STORE
//#region SERVICE
/**
* Sets up a useful API surface and background
* checks for certain meta-data which is available
* as a Pinia store.
*/
export const install: UserModule = ({ isClient }) => {
if (isClient) {
const s = useSearch();
const { pause, resume, isActive } = useIntervalFn(async() => {
s.indexUpdate(await indexes());
s.statsUpdate(await stats());
}, 2000, {immediate: true});
// check health of MeiliSearch
useIntervalFn(async() => {
const h = await health();
s.healthUpdate(h);
s.$state.searchStatus = !h ? "not-ready": "ready";
if(h === true && !isActive) resume();
if(h === false && isActive) pause();
}, 1000, {immediate: true});
}
};
async function get<T extends {}, U extends {} = never>(
url: string,
cb?: ((r: T) => U)
) {
const res = await fetch(url);
if(res.ok) {
const result = res.json();
return (
cb
? result.then(r => cb(r)) as Promise<U>
: result as Promise<T>
) as never extends U ? Promise<T> : Promise<U>;
} else {
console.groupCollapsed(`Error with API`);
console.info(`Request: GET ${url}`);
console.warn(`Error [${res.status}]: ${res.statusText}`);
console.groupEnd();
}
}
function api(base: string = "http://localhost:7700") {
return {
search: (idx: string, text: string) => `${base}/indexes/${idx}/search?q=${text}`,
stats: `${base}/stats`,
health: `${base}/health`,
indexes: `${base}/indexes`
};
}
async function health(): Promise<boolean> {
const response = await (
await fetch(api().health)
).json() as MeiliSearchHealth;
return response.status === "available";
}
async function indexes() {
return await (
await fetch(api().indexes)
).json() as MeiliSearchInterface[];
}
async function stats() {
return await (
await fetch(api().stats)
).json() as MeiliSearchStats;
}
//#endregion

View File

@@ -2,16 +2,24 @@
+++ Install Dependencies and Start Docs Site (_looks like you already did_)
```bash
# install npm dependencies (in all repos)
# 1. installs deps for both CLI and Docs
# 2. starts Docs server (in dev mode with HMR), opens in browser
# 3. starts Meilisearch server in Docker
pnpm run start
# start Docker services (you must have Docker installed)
docker compose up
# bring up documentation / playground site
pnpm run dev
```
A browser window should now have openned up pointing to `http://localhost:3333`.
You are now up and running with the documentation site and assuming your Docker services also started up -- the [search server](./meilisearch) and [scraper utility](./scraper) -- then you will be able to start trying out searches immediately.
> A browser window should now have opened to [`http://localhost:3333`](http://localhost:3333).
You are now up and running with the documentation site -- and assuming you have Docker installed -- a local [search server](./meilisearch) which you can interact with.
### Already installed Deps?
If you've already installed all the deps and want more granular control you can choose from the various script targets or just choose _watch_ to look at docs with active editing capability:
```bash
# turn on watcher mode for both CLI and Docs
pnpm run watch
```
+++

View File

@@ -1,37 +1,32 @@
<script setup lang="ts">
import { onStartTyping, useIntervalFn } from '@vueuse/core'
const el = ref()
const searchText = ref('')
import { onStartTyping } from "@vueuse/core";
import { useSearch } from "~/modules/search";
const el = ref();
const searchText = ref("");
const s = useSearch();
const { t } = useI18n();
debouncedWatch(
searchText,
async() => {
await s.search(searchText.value);
},
{ debounce: 500 }
);
onStartTyping(() => {
if (!el.value.active)
el.value.focus()
})
if (!el.value.active) {el.value.focus();};
});
const searchAvailable = ref(false)
useIntervalFn(async() => {
const response = await (await fetch('http://localhost:7700/health')).json() as { status: string }
searchAvailable.value = response.status === 'available'
}, 5000)
const router = useRouter()
const search = async() => {
const response = await (await fetch(`http://localhost:7700/indexes/repo/search?q=${searchText.value}`)).json()
}
const { t } = useI18n()
</script>
<template>
<div class="py-8 w-full flex flex-col space-y-4">
<div class="text-xl">
Use the local Meilisearch here.
</div>
<div class="text-xs" :class="searchAvailable ? 'text-green-500' : 'text-red-500'">
<span v-if="searchAvailable">Meilisearch Found Locally!</span>
<span v-else>Meilisearch is not available locally!</span>
</div>
<div class="py-3 w-full h-full">
<h1 class="text-xl mb-4">
Meilisearch Playground
</h1>
<input
id="input"
ref="el"
@@ -40,25 +35,22 @@ const { t } = useI18n()
:aria-label="t('home.ask-for-search')"
type="text"
autocomplete="false"
class="px-auto px-4 py-2 w-250px text-center bg-transparent self-center"
spellcheck="false"
class="px-auto px-4 py-3 w-350px text-center bg-transparent self-center"
border="~ rounded gray-200 dark:gray-700"
outline="none active:none"
@keydown.enter="search"
>
<label class="hidden" for="input">{{ t('home.ask-for-search') }}</label>
<div>
<button
class="m-3 text-sm btn"
:disabled="!searchText"
@click="search"
>
{{ t('button.go') }}
</button>
</div>
<div class="results-area grid grid-cols-3 gap-4 h-full mt-8 mx-4">
<div class="flex flex-col col-span-1 space-y-4">
<search-actions />
<search-stats class="mt-4" />
</div>
<div class="results-area flex flex-col flex-grow">
results
<div class="col-span-2 rounded-md bg-gray-100/25 dark:bg-gray-900/25">
<search-results :query="searchText" />
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,59 @@
export interface MeiliSearchHealth {
/** the current health status; is "available" when healthy */
status: string;
}
export type datetime = string;
export interface MeiliSearchInterface {
uid: string;
name: string;
/** datetime string (aka., 2022-01-23T22:47:42.745395044Z) */
createdAt: datetime;
/** datetime string (aka., 2022-01-23T22:47:42.745395044Z) */
updatedAt: datetime;
/**
* the property serving as the primary key for the index,
* use `id` unless there's a good reason not to
*/
primaryKey: string;
}
export interface MeiliSearchIndex {
numberOfDocuments: number;
isIndexing: false;
fieldDistribution: Record<string, any>;
}
/**
* Global MeiliSearch Stats
*/
export interface MeiliSearchStats {
databaseSize: number;
lastUpdate: datetime;
indexes: Record<string, MeiliSearchIndex>;
}
export type GenericDoc = {id: string; _idx?: string; [key:string]: unknown};
export interface MeiliSearchResponse<T extends {} = GenericDoc> {
hits: T[];
limit: number;
nbHits: number;
offset: number;
processingTimeMs: number;
query: string;
}
/**
* The search results but _with_ the index used inserted on
* each "hit"
*/
export type WithIndex<T extends MeiliSearchResponse> =
Omit<T, "hits"> & { hits:
{
[K in keyof T["hits"]]: T["hits"][K] & Record<K, {_idx: string}>
}[number];
};

View File

@@ -19,7 +19,7 @@
"vite-plugin-vue-layouts/client"
],
"paths": {
"~/*": ["src/*"]
"~/*": ["src/*"],
}
},
"include": ["src", "test", "vite.config.ts"],

View File

@@ -8,11 +8,11 @@ const config: Config.InitialOptions = {
transform: {
"^.+\\.ts$": "ts-jest",
},
testPathIgnorePatterns: ["/node_modules/"],
testPathIgnorePatterns: ["/node_modules/", "/docs/"],
moduleNameMapper: {
"^[/]{0,1}~/(.*)$": resolve(process.cwd(), "src", "$1"),
},
testMatch: ["test/**/?(*[-.])+(spec|test).ts"],
testMatch: ["**/?(*[-.])+(spec|test).ts"],
setupFilesAfterEnv: ["jest-extended"],
testEnvironment: "node",
};

View File

@@ -7,10 +7,12 @@
"ts-ast": "./bin/ts-ast.cjs"
},
"scripts": {
"start": "run-p start:*",
"start:seach": "pnpm install",
"start:docs": "pnpm -C ./docs/ pnpm install",
"up": "docker compose up",
"start": "run-s prep watch",
"prep": "run-p start:*",
"start:cli": "pnpm install",
"start:docs": "pnpm -C docs start",
"start:docker": "run-s up",
"up": "docker compose up -d",
"down": "docker compose down",
"restart": "docker compose restart",
"vol:inspect": "run-s vol:inspect:*",
@@ -23,25 +25,29 @@
"into:scraper": "docker exec -it scraper bash",
"ping": "http GET localhost:7700/health",
"prune": "docker system prune",
"mountpoint": "bash scripts/mountpoint.bash",
"dev": "pnpm -C demo pnpm run dev",
"dev": "pnpm -C docs dev",
"lint": "eslint src --ext ts,js,tsx,jsx --fix --no-error-on-unmatched-pattern",
"test": "echo \"Error: no test specified\" && exit 1",
"ts-ast-overview": "node ./bin/ts-ast-overview.js",
"ts-ast": "node ./bin/ts-ast.js",
"watch": "run-p watch:*",
"watch:docs": "pnpm -C docs/ run dev",
"watch:cli": "tsup src/cli/*.ts --format=esm,cjs --clean --sourcemap -d bin --watch"
"watch:docs": "pnpm -C docs dev",
"watch:cli": "tsup src/cli/*.ts --format=esm,cjs --clean --sourcemap -d bin --watch",
"watch:types": "tsup src/models/index.ts src/type/index.ts --dts"
},
"author": "Ken Snyder<ken@ken.net>",
"license": "MIT",
"engines": {
"node": ">=14",
"pnpm": ">=3"
},
"dependencies": {
"cheerio": "^1.0.0-rc.10",
"gray-matter": "^4.0.3",
"inferred-types": "^0.18.4",
"native-dash": "^1.21.5",
"node-fetch": "^3.1.1",
"ts-morph": "^13.0.2",
"node-fetch": "^3.2.0",
"ts-morph": "^13.0.3",
"xxhash-wasm": "^1.0.1"
},
"devDependencies": {
@@ -49,17 +55,18 @@
"@octokit/types": "^6.34.0",
"@types/jest": "^27.4.0",
"@types/markdown-it": "^12.2.3",
"@types/node": "14",
"@typescript-eslint/eslint-plugin": "^5.9.1",
"@typescript-eslint/parser": "^5.9.1",
"@types/node": "^14.18.9",
"@typescript-eslint/eslint-plugin": "^5.10.0",
"@typescript-eslint/parser": "^5.10.0",
"eslint": "^8.7.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-cypress": "^2.12.1",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-jest": "^25.3.4",
"eslint-plugin-jest": "^25.7.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-promise": "^6.0.0",
"fx": "^20.0.2",
"husky": "^7.0.4",
"jest": "^27.4.7",
"jest-extended": "^1.2.0",
"markdown-it": "^12.3.2",
@@ -73,6 +80,6 @@
"ts-jest": "^27.1.3",
"ts-node": "^10.4.0",
"tsup": "^5.11.11",
"typescript": "^4.6.0-dev.20220117"
"typescript": "^4.6.0-dev.20220124"
}
}

460
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
import { RepoModel } from "~/models/RepoModel";
import { IRepoModel } from "~/models/RepoModel";
import { url } from "~/types/aliases";
import { createMapper } from "~/utils/createMapper";
import { GithubRepoResp } from "~/utils/github/getRepo";
@@ -6,7 +6,7 @@ import { GithubRepoResp } from "~/utils/github/getRepo";
/**
* Maps a **Typescript Module** to the Meilisearch documents for that module.
*/
export const githubMapper = createMapper<GithubRepoResp["data"], RepoModel>(
export const githubMapper = createMapper<GithubRepoResp["data"], IRepoModel>(
"githubMapper",
(i) => ({
id: `github_${i.full_name.replace("/", "_")}`,

View File

@@ -24,6 +24,6 @@ export const tsFunction = createMapper<TsAstFunction, ApiModel>("tsFunction", (i
type: s.type.name,
comment: s.comment?.text || s.comment?.shortText,
})),
declaration: `function ${i.name}(${i.signature.map((s) => `s.name`).join(", ")})`,
declaration: `function ${i.name}(${i.signature.map((s) => `${s.name}`).join(", ")})`,
url: `${TAURI_JS_DOCS_URL}/modules/${i.module}#${i.name}`,
}));

View File

@@ -2,7 +2,7 @@ import { TypescriptKind } from "~/enums";
import { en } from "~/stop-words";
import { createModel } from "~/utils/createModel";
export interface ApiModel {
export interface IApiModel {
id: string;
language: "rust" | "typescript";
/**
@@ -29,10 +29,9 @@ export interface ApiModel {
parameters?: { name: string; type?: string; comment?: string; kind: TypescriptKind }[];
}
const model = createModel<ApiModel>("api", (c) =>
export const ApiModel = createModel<IApiModel>("api", (c) =>
c //
.stopWords(en)
.filterable("language", "kind", "module", "tags")
);
export default model;

View File

@@ -1,7 +1,7 @@
import { datetime, url } from "~/types/aliases";
import { createModel } from "~/utils/createModel";
export interface RepoModel {
export interface IRepoModel {
id: string;
name: string;
description: string | null;
@@ -28,4 +28,4 @@ export interface RepoModel {
url: url;
}
export const RepoModel = createModel<RepoModel>("repo");
export const RepoModel = createModel<IRepoModel>("repo");

2
src/models/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export * from "./RepoModel";
export * from "./ApiModel";

View File

@@ -1,4 +1,4 @@
import api from "~/models/api";
import {ApiModel} from "~/models";
import {
tsClass,
tsFunction,
@@ -20,7 +20,7 @@ import { tsEnumeration } from "~/mappers/tsEnumeration";
export async function typescriptPipeline() {
// get AST from output of TSDoc's JSON option
const ast = await parseTypescriptAst();
const model = await api;
const model = await ApiModel;
const waitFor: Promise<MsAddOrReplace>[] = [];

View File

@@ -147,22 +147,6 @@ export type PropCharacteristicsApi<E extends string> = Omit<
E
>;
// name: p => p.string().searchable().
const prose: ModelDefnApi = {
document: {
foo: (m) => m.string("foo", "bar").displayed().searchable(),
bar: (m) => m.boolean().searchable(),
baz: (m) => m.number(),
},
index: {
rankingRules: (r) => r.words().attribute().exactness(),
stopWords: [],
synonyms: {
ts: ["typescript"],
typescript: ["ts"],
},
},
};
export type SearchModel<N extends string, T extends any> = {
/** the name of the model */

View File

@@ -8,7 +8,6 @@ import {
MsSettingsResponse,
RankingRule,
RankingRulesApi,
StopWords,
} from "~/types";
import xxhash from "xxhash-wasm";
import fetch from "node-fetch";
@@ -122,7 +121,7 @@ export type SearchModel<T extends {}> = {
filterable?: (keyof T)[];
distinct?: (keyof T)[];
sortable?: (keyof T)[];
stopWords?: StopWords;
stopWords?: string[];
synonyms?: IndexSynonyms;
};
query: {

View File

@@ -6,6 +6,7 @@ import { parseFunction } from "./parseFunction";
import { parseInterface } from "./parseInterface";
import { parseReference } from "./parseReference";
import { parseTypeAlias } from "./parseTypeAlias";
import { parseVariable } from "./parseVariable";
export function parseModule(mod: TypescriptBlock) {
const modDefn: TsAstModule = {
@@ -25,7 +26,7 @@ export function parseModule(mod: TypescriptBlock) {
for (const i of mod.children || []) {
switch (i.kindString) {
case TypescriptKind.Enumeration:
modDefn.enums.push(parseEnumeration(mod, i));
modDefn.enums.push(parseEnumeration(mod.name, i));
break;
case TypescriptKind.Function:
modDefn.functions.push(parseFunction(mod.name, i));
@@ -37,7 +38,7 @@ export function parseModule(mod: TypescriptBlock) {
modDefn.classes.push(parseClass(mod.name, i));
break;
case TypescriptKind.Variable:
modDefn.variables.push(i);
modDefn.variables.push(parseVariable(mod.name, i));
break;
case TypescriptKind.TypeAlias:
modDefn.typeAliases.push(parseTypeAlias(mod.name, i));

12
test/frontmatter.test.ts Normal file
View File

@@ -0,0 +1,12 @@
import { readFileSync } from "fs";
import matter from "gray-matter";
describe("frontmatter tests", () => {
it("faq", () => {
const content = readFileSync("test/fixtures/prose/faq.md", "utf-8");
const output = matter(content);
console.log(output)
});
});

View File

@@ -1,23 +1,19 @@
{
"compilerOptions": {
"module": "ESNext",
"target": "es2016",
"target": "es2019",
"lib": ["DOM", "ESNext"],
"moduleResolution": "node",
"strict": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"incremental": false,
"skipLibCheck": true,
"resolveJsonModule": true,
"noUnusedLocals": true,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"types": [
"cypress",
"vite/client",
"vite-plugin-pages/client",
"vite-plugin-vue-layouts/client"
],
"types": [],
"baseUrl": ".",
"paths": {