Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(specs): add playground #5973

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
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
11 changes: 11 additions & 0 deletions specs/astro.config.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
import react from '@astrojs/react';
import { defineConfig } from 'astro/config';

export default defineConfig({
site: 'https://instantsearchjs.netlify.app/',
base: '/specs',
outDir: '../website/specs',
integrations: [react()],
vite: {
ssr: {
noExternal: [
'@codesandbox/sandpack-react',
'@codesandbox/sandpack-themes',
'@codesandbox/sandpack-client',
],
},
},
});
9 changes: 8 additions & 1 deletion specs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,16 @@
"prepare": "rm -rf public && mkdir public && cp -r ../node_modules/instantsearch.css/themes public/themes"
},
"devDependencies": {
"@types/node": "18.11.13",
"@types/node": "20.10.0",
"@codesandbox/sandpack-react": "2.19.1",
"@codesandbox/sandpack-themes": "2.0.21",
"dedent": "1.5.1",
"astro": "1.6.14",
"@astrojs/react": "1.2.2",
"instantsearch.css": "8.5.0",
"instantsearch.js": "4.74.0",
"react-instantsearch": "7.13.0",
"vue-instantsearch": "4.19.3",
"sass": "1.56.2"
}
}
207 changes: 207 additions & 0 deletions specs/src/components/Sandbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
/* eslint-disable react/react-in-jsx-scope */
import { Sandpack } from '@codesandbox/sandpack-react';
import { githubLight } from '@codesandbox/sandpack-themes';
import dedent from 'dedent';

const settings = {
js: {
html: /* HTML */ `
<style>
body {
font-family: sans-serif;
}
</style>
<div id="custom"></div>
<hr />
<div id="searchbox"></div>
<div id="hits"></div>
`,
preamble: /* JS */ `
import 'instantsearch.css/themes/satellite-min.css';
import { liteClient as algoliasearch } from 'algoliasearch/lite';
import instantsearch from 'instantsearch.js';
import { history } from 'instantsearch.js/es/lib/routers';
import { searchBox, hits } from 'instantsearch.js/es/widgets';
import { createWidgets } from './widget.ts';

const search = instantsearch({
indexName: 'instant_search',
searchClient: algoliasearch('latency', '6be0576ff61c053d5f9a3225e2a90f76'),
future: {
preserveSharedStateOnUnmount: true,
},
routing: {
router: history({
cleanUrlOnDispose: false,
}),
},
});

search.addWidgets([
...createWidgets(() =>
document.querySelector('#custom').appendChild(document.createElement('div'))
),
searchBox({
container: '#searchbox',
}),
hits({
container: '#hits',
templates: {
item: (hit, { components }) =>
components.Highlight({ attribute: 'name', hit }),
},
}),
]);

search.start();
`,
dependencies: {
algoliasearch: '5.1.1',
},
filename: '/widget.ts',
},
react: {
html: /* HTML */ `
<style>
body {
font-family: sans-serif;
}
</style>
<main id="root"></main>
`,
preamble: /* TSX */ `
import 'instantsearch.css/themes/satellite-min.css';
import React from "react";
import { createRoot } from "react-dom/client";
import { liteClient as algoliasearch } from "algoliasearch/lite";
import { history } from "instantsearch.js/es/lib/routers";
import { InstantSearch, SearchBox, Hits, Highlight } from "react-instantsearch";
import { widgets } from "./widget.tsx";

createRoot(document.getElementById('root')).render(
<InstantSearch
indexName="instant_search"
searchClient={algoliasearch('latency', '6be0576ff61c053d5f9a3225e2a90f76')}
future={{
preserveSharedStateOnUnmount: true,
}}
routing={{
router: history({
cleanUrlOnDispose: false,
})
}}
>
{widgets}
<hr />
<SearchBox />
<Hits hitComponent={Hit}/>
</InstantSearch>
);

function Hit({ hit }) {
return <Highlight hit={hit} attribute="name" />;
}
`,
dependencies: {
react: '18.2.0',
'react-dom': '18.2.0',
algoliasearch: '5.1.1',
},
filename: '/widget.tsx',
},
vue: {
html: /* HTML */ `
<style>
body {
font-family: sans-serif;
}
</style>
<main id="app"></main>
`,
preamble: `
import "instantsearch.css/themes/satellite-min.css";
import Vue from "vue";
import { liteClient as algoliasearch } from "algoliasearch/lite";
import { history } from "instantsearch.js/es/lib/routers";
import { AisInstantSearch, AisHits, AisSearchBox } from "vue-instantsearch/vue2/es";
import Widget from "./Widget.vue";

Vue.config.productionTip = false;

new Vue({
render: (h) =>
h(
AisInstantSearch,
{
props: {
searchClient: algoliasearch(
"latency",
"6be0576ff61c053d5f9a3225e2a90f76"
),
indexName: "instant_search",
future: {
preserveSharedStateOnUnmount: true,
},
routing: {
router: history({
cleanUrlOnDispose: false,
})
}
},
},
[h(Widget), h('hr'), h(AisSearchBox), h(AisHits)]
),
}).$mount("#app");
`,
dependencies: {
vue: '2.7.14',
algoliasearch: '5.1.1',
},
filename: '/Widget.vue',
},
};

export default function Sandbox({
code,
flavor,
modules,
}: {
code: string;
flavor: 'react' | 'js' | 'vue';
modules: {
files: Record<string, string>;
dependencies: Record<string, string>;
};
}) {
const { preamble, html, filename, dependencies } = settings[flavor];
return (
<Sandpack
files={{
'/index.html': {
hidden: true,
code: dedent(html),
},
'/index.js': {
code: dedent(preamble),
},
[filename]: {
code,
},
...modules.files,
}}
customSetup={{
dependencies: {
...dependencies,
...modules.dependencies,
},
entry: '/index.js',
}}
options={{
editorHeight: 500,
activeFile: filename,
showNavigator: true,
}}
theme={githubLight}
/>
);
}
32 changes: 31 additions & 1 deletion specs/src/components/WidgetContent.astro
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
import { Code } from 'astro/components';
import type { WidgetFrontmatter } from '../types';
import ThemeSelector from './ThemeSelector.astro';
import Sandbox from './Sandbox.jsx';
import { getSandpackCssText } from '@codesandbox/sandpack-react';
import getFiles from '../getDependencyContent';

const modules = await getFiles();

type Props = {
frontmatter: WidgetFrontmatter;
Expand All @@ -23,7 +28,8 @@ const title = frontmatter.title;
{
frontmatter.info && (
<div class="info">
<p>{frontmatter.info}</p>
{' '}
<p>{frontmatter.info}</p>{' '}
</div>
)
}
Expand Down Expand Up @@ -110,6 +116,30 @@ const title = frontmatter.title;
)
}

{
frontmatter.examples ? (
<>
<h3 id="example">Usage</h3>
<style id="sandpack" is:inline set:html={getSandpackCssText()} />
{frontmatter.examples.map(({ code, flavor, library }) => (
<details class="example">
<summary style="cursor: pointer">
<h4 style="display: inline-block; margin: 0;">{library}</h4>
</summary>
<div class="code-output">
<Sandbox
code={code}
flavor={flavor}
modules={modules[flavor]}
client:load
/>
</div>
</details>
))}
</>
) : null
}

<h3 id="css-classes">CSS classes</h3>
{
frontmatter.classes ? (
Expand Down
1 change: 1 addition & 0 deletions specs/src/env.d.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
/// <reference path="../.astro/types.d.ts" />
// / <reference types="astro/client" />
92 changes: 92 additions & 0 deletions specs/src/getDependencyContent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { readdir, readFile } from 'fs/promises';
import { join } from 'path';

const cache = new Map();
async function getFilesFromDir(dir: string) {
if (cache.has(dir)) {
return cache.get(dir);
}
const output = (
await readdir(dir, {

Check warning on line 10 in specs/src/getDependencyContent.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

specs/src/getDependencyContent.ts#L10

Detected that function argument `dir` has entered the fs module.
withFileTypes: true,
recursive: true,
})
)
.filter((file) => file.isFile())
.map((file) => join(file.path.replace('../packages/', ''), file.name));

Check warning on line 16 in specs/src/getDependencyContent.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

specs/src/getDependencyContent.ts#L16

Detected possible user input going into a `path.join` or `path.resolve` function.
cache.set(dir, output);
return output;
}

async function getFiles(flavor: 'react' | 'js' | 'vue') {
const localDependencies = [
'instantsearch.css',
'instantsearch-ui-components',
'instantsearch.js',
flavor === 'react' && 'react-instantsearch',
flavor === 'react' && 'react-instantsearch-core',
flavor === 'vue' && 'vue-instantsearch',
].filter(Boolean);

return {
files: Object.fromEntries(
await Promise.all(
[
'instantsearch.css/package.json',
...(await getFilesFromDir('../packages/instantsearch.css/themes')),
'instantsearch-ui-components/package.json',
...(await getFilesFromDir(
'../packages/instantsearch-ui-components/dist'
)),
'instantsearch.js/package.json',
...(await getFilesFromDir('../packages/instantsearch.js/es')),
...(flavor === 'react'
? await getFilesFromDir('../packages/instantsearch.js/cjs')
: []),
flavor === 'react' && 'react-instantsearch/package.json',
...(flavor === 'react'
? await getFilesFromDir('../packages/react-instantsearch/dist')
: []),
flavor === 'react' && 'react-instantsearch-core/package.json',
...(flavor === 'react'
? await getFilesFromDir('../packages/react-instantsearch-core/dist')
: []),
flavor === 'vue' && 'vue-instantsearch/package.json',
...(flavor === 'vue'
? await getFilesFromDir('../packages/vue-instantsearch/vue2/es')
: []),
]
.filter(Boolean)
.map(async (file) => [
`/node_modules/${file}`,
{
code: await readFile(`../packages/${file}`, 'utf-8'),

Check warning on line 63 in specs/src/getDependencyContent.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

specs/src/getDependencyContent.ts#L63

Detected that function argument `file` has entered the fs module.
hidden: true,
},
])
)
),
dependencies: Object.assign(

Check warning on line 69 in specs/src/getDependencyContent.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

specs/src/getDependencyContent.ts#L69

Depending on the context, user control data in `Object.assign` can cause web response to include data that it should not have or can lead to a mass assignment vulnerability.
{},
...(await Promise.all(
localDependencies.map(async (pkg) =>
Object.fromEntries(
Object.entries(
JSON.parse(
await readFile(`../packages/${pkg}/package.json`, 'utf-8')

Check warning on line 76 in specs/src/getDependencyContent.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

specs/src/getDependencyContent.ts#L76

Detected that function argument `pkg` has entered the fs module.
).dependencies || {}
).filter(([key]) => !localDependencies.includes(key))
)
)
))
),
};
}

export default async function getDependencyContent() {
return {
react: await getFiles('react'),
js: await getFiles('js'),
vue: await getFiles('vue'),
};
}
Loading