Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions .vitepress/theme/index.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import DefaultTheme from 'vitepress/theme';
import { h } from 'vue';
import { defineAsyncComponent, h } from 'vue';

import './variables.css';
import './main.css';
import 'virtual:group-icons.css';
import ApiOutline from '../../components/ApiOutline.vue';
import CarbonAds from '../../components/CarbonAds.vue';
import ModuleIndex from '../../components/ModuleIndex.vue';
import StatusContent from '../../components/StatusContent.vue';
import TesterContent from '../../components/TesterContent.vue';
import { getRedirectPath } from './redirect.js';

import type { Theme } from 'vitepress';

const ApiOutline = defineAsyncComponent(() => import('../../components/ApiOutline.vue'));
const CarbonAds = defineAsyncComponent(() => import('../../components/CarbonAds.vue'));
const ModuleIndex = defineAsyncComponent(() => import('../../components/ModuleIndex.vue'));
const StatusContent = defineAsyncComponent(() => import('../../components/StatusContent.vue'));
const TesterContent = defineAsyncComponent(() => import('../../components/TesterContent.vue'));

export default {
Layout() {
return h(DefaultTheme.Layout, null, {
Expand Down
37 changes: 34 additions & 3 deletions components/CodeMirrorEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,24 @@
</template>

<script setup>
import { autocompletion } from '@codemirror/autocomplete';
import { javascript } from '@codemirror/lang-javascript';
import { json } from '@codemirror/lang-json';
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
import { Compartment, EditorState, StateEffect, StateField } from '@codemirror/state';
import { Decoration } from '@codemirror/view';
import { Decoration, tooltips } from '@codemirror/view';
import { tags as t } from '@lezer/highlight';
import { darcula } from '@uiw/codemirror-theme-darcula';
import { eclipse } from '@uiw/codemirror-theme-eclipse';
import { EditorView, basicSetup } from 'codemirror';
import { useData } from 'vitepress';
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';

const { errorLines, language, readOnly } = defineProps({
import { joiCompletionSource } from '../composables/joiCompletionSource.ts';

const { errorLines, joiVersion, language, readOnly } = defineProps({
errorLines: { default: () => [], type: Array },
joiVersion: { default: null, type: String },
language: { default: 'javascript', type: String },
readOnly: { default: false, type: Boolean },
});
Expand All @@ -31,6 +35,7 @@ let view = null;


const themeCompartment = new Compartment();
const joiCompartment = new Compartment();
const setErrorLines = StateEffect.define();


Expand Down Expand Up @@ -72,12 +77,23 @@ const getThemeExtension = () => {
};


const getJoiExtension = () => {
if (!joiVersion) {
return [];
}
return autocompletion({
override: [(context) => joiCompletionSource(context, joiVersion)],
});
};


onMounted(() => {
const extensions = [
basicSetup,
language === 'json' ? json() : javascript(),
EditorView.lineWrapping,
themeCompartment.of(getThemeExtension()),
joiCompartment.of(getJoiExtension()),
EditorView.updateListener.of((update) => {
if (update.docChanged) {
data.value = update.state.doc.toString();
Expand Down Expand Up @@ -136,6 +152,18 @@ watch(isDark, () => {
});


watch(
() => joiVersion,
() => {
if (view) {
view.dispatch({
effects: joiCompartment.reconfigure(getJoiExtension()),
});
}
},
);


onBeforeUnmount(() => {
if (view) {
view.destroy();
Expand All @@ -147,8 +175,11 @@ onBeforeUnmount(() => {
.editor-container {
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
overflow: hidden;
font-family: var(--vp-font-family-mono);
font-size: 14px;
}

.editor-container :deep(.cm-tooltip-autocomplete > ul) {
max-height: 350px;
}
</style>
4 changes: 2 additions & 2 deletions components/TesterContent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

<div class="field">
<h2 class="tester-subTitle">Schema:</h2>
<CodeMirrorEditor v-model="schema" />
<CodeMirrorEditor v-model="schema" :joi-version="version" />
</div>

<div class="field">
Expand Down Expand Up @@ -71,7 +71,7 @@
<script setup>
import { useClipboard, useStorage } from '@vueuse/core';
import { useRoute } from 'vitepress';
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { onMounted, ref, watch } from 'vue';

import { annotate } from '../composables/annotate.ts';
import joiInfo from '../generated/modules/joi/info.json' with { type: 'json' };
Expand Down
126 changes: 126 additions & 0 deletions composables/joiCompletionSource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { createDefaultMapFromCDN, createSystem, createVirtualTypeScriptEnvironment } from '@typescript/vfs';

import type { CompletionContext } from '@codemirror/autocomplete';
import type { VirtualTypeScriptEnvironment } from '@typescript/vfs';

let env: VirtualTypeScriptEnvironment | null = null;
let currentVersion: string | null = null;

const getEnv = async (version: string) => {
if (env && currentVersion === version) {
return env;
}

const ts = await import('typescript');

const compilerOptions = {
lib: ['es2024'],
module: ts.ModuleKind.ESNext,
target: ts.ScriptTarget.ESNext,
};

const fsMap = await createDefaultMapFromCDN(compilerOptions, ts.version, true, ts);

const { default: joiDts } = version.startsWith('17.')
? await import('joi-17/lib/index.d.ts?raw')
: await import('joi-18/lib/index.d.ts?raw');

fsMap.set('/node_modules/joi/index.d.ts', joiDts);
fsMap.set('/node_modules/joi/package.json', JSON.stringify({ name: 'joi', types: 'index.d.ts' }));

if (version.startsWith('18.')) {
const { default: standardSchemaTypes } = await import('../node_modules/@standard-schema/spec/dist/index.d.ts?raw');
fsMap.set('/node_modules/@standard-schema/spec/index.d.ts', standardSchemaTypes);
fsMap.set(
'/node_modules/@standard-schema/spec/package.json',
JSON.stringify({ name: '@standard-schema/spec', types: 'index.d.ts' }),
);
}

const system = createSystem(fsMap);
env = createVirtualTypeScriptEnvironment(system, [], ts, compilerOptions);
currentVersion = version;

return env;
};

const filterJoiOperations = (name: string) =>
name.startsWith('$') ||
name.startsWith('_') ||
name.startsWith('validate') ||
name === 'cache' ||
name === 'ValidationError';

export const joiCompletionSource = async (context: CompletionContext, version: string) => {
try {
const tsEnv = await getEnv(version);
const code = context.state.doc.toString();

const prefix = "import Joi from 'joi';\n";
const wrappedCode = prefix + code;
const pos = context.pos + prefix.length;

const fileName = 'index.ts';
if (tsEnv.getSourceFile(fileName)) {
tsEnv.updateFile(fileName, wrappedCode);
} else {
tsEnv.createFile(fileName, wrappedCode);
}

const completions = tsEnv.languageService.getCompletionsAtPosition(fileName, pos, {});
if (!completions) {
return null;
}

const word = context.matchBefore(/\w*/);
if (!word && !context.explicit) {
return null;
}

return {
from: word ? word.from : context.pos,
options: completions.entries
.filter((entry) => !filterJoiOperations(entry.name))
.map((entry) => {
let type = 'variable';
if (entry.kind === 'method') {
type = 'method';
} else if (entry.kind === 'property') {
type = 'property';
}

return {
boost: entry.sortText ? -Number(entry.sortText) : 0,
info: () => {
const details = tsEnv.languageService.getCompletionEntryDetails(
fileName,
pos,
entry.name,
{},
entry.source,
{},
entry.data,
);
if (!details) {
return null;
}

const doc = `${entry.kind} ${entry.name}
${details.documentation?.map((d) => d.text).join('\n') || ''}`;

const div = document.createElement('div');
div.className = 'cm-completionInfo-text';
div.style.whiteSpace = 'pre-wrap';
div.textContent = doc;
return div;
},
label: entry.name,
type,
};
}),
};
} catch (error) {
console.error('Joi completion error:', error);
return null;
}
};
5 changes: 5 additions & 0 deletions env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,8 @@ declare module '*.vue' {
const component: DefineComponent<{}, {}, any>;
export default component;
}

declare module '*?raw' {
const content: string;
export default content;
}
24 changes: 12 additions & 12 deletions generated/metadata/modules.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"address": {
"forks": 27,
"link": "https://github.com/hapijs/address",
"package": "@hapi/address",
"slogan": "Validate email address and domain.",
"stars": 33,
"updated": "2024-01-29T12:37:22Z",
Expand All @@ -13,12 +14,12 @@
"node": ">= 14"
}
],
"versionsArray": ["5.1.1"],
"package": "@hapi/address"
"versionsArray": ["5.1.1"]
},
"formula": {
"forks": 20,
"link": "https://github.com/hapijs/formula",
"package": "@hapi/formula",
"slogan": "Math and string formula parser.",
"stars": 17,
"updated": "2024-02-02T16:21:44Z",
Expand All @@ -30,12 +31,12 @@
"node": ">= 14"
}
],
"versionsArray": ["3.0.2"],
"package": "@hapi/formula"
"versionsArray": ["3.0.2"]
},
"joi": {
"forks": 1509,
"link": "https://github.com/hapijs/joi",
"package": "joi",
"slogan": "The most powerful schema description language and data validator for JavaScript.",
"stars": 21199,
"updated": "2026-03-23T17:51:24Z",
Expand All @@ -53,12 +54,12 @@
"node": ">= 20"
}
],
"versionsArray": ["18.1.1", "17.13.3"],
"package": "joi"
"versionsArray": ["18.1.1", "17.13.3"]
},
"joi-date": {
"forks": 23,
"link": "https://github.com/hapijs/joi-date",
"package": "@joi/date",
"slogan": "Extensions for advance date rules.",
"stars": 81,
"updated": "2024-04-22T09:05:34Z",
Expand All @@ -70,12 +71,12 @@
"node": ">= 14"
}
],
"versionsArray": ["2.1.1"],
"package": "@joi/date"
"versionsArray": ["2.1.1"]
},
"pinpoint": {
"forks": 6,
"link": "https://github.com/hapijs/pinpoint",
"package": "@hapi/pinpoint",
"slogan": "Return the filename and line number of the calling function.",
"stars": 6,
"updated": "2023-11-12T09:48:56Z",
Expand All @@ -87,12 +88,12 @@
"node": ">= 14"
}
],
"versionsArray": ["2.0.1"],
"package": "@hapi/pinpoint"
"versionsArray": ["2.0.1"]
},
"tlds": {
"forks": 2,
"link": "https://github.com/hapijs/tlds",
"package": "@hapi/tlds",
"slogan": "TLDS list for domain validation.",
"stars": 2,
"updated": "2026-02-17T14:09:37Z",
Expand All @@ -104,7 +105,6 @@
"node": ">= 14"
}
],
"versionsArray": ["1.1.6"],
"package": "@hapi/tlds"
"versionsArray": ["1.1.6"]
}
}
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,22 @@
"license": "BSD-3-Clause",
"dependencies": {
"@babel/runtime": "^7.29.2",
"@codemirror/autocomplete": "^6.20.1",
"@codemirror/lang-javascript": "^6.2.5",
"@codemirror/lang-json": "^6.0.2",
"@codemirror/language": "^6.12.2",
"@codemirror/state": "^6.6.0",
"@codemirror/view": "^6.40.0",
"@lezer/highlight": "^1.2.3",
"@typescript/vfs": "^1.6.4",
"@uiw/codemirror-theme-darcula": "^4.25.8",
"@uiw/codemirror-theme-eclipse": "^4.25.8",
"@vueuse/core": "^14.2.1",
"codemirror": "^6.0.2",
"es-toolkit": "^1.45.1",
"joi-17": "npm:joi@17.13.3",
"joi-18": "npm:joi@18.0.2",
"@standard-schema/spec": "^1.1.0",
"semver": "^7.7.4",
"vitepress-plugin-group-icons": "^1.7.1",
"vue": "^3.5.30"
Expand Down
Loading