Skip to content

Commit

Permalink
Merge pull request #157 from Shopify/compile-examples-for-playground
Browse files Browse the repository at this point in the history
  • Loading branch information
iainmcampbell authored Aug 5, 2021
2 parents d3575f8 + 941a578 commit 1b1361e
Show file tree
Hide file tree
Showing 10 changed files with 463 additions and 21 deletions.
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"devDependencies": {
"@babel/node": "^7.8.7",
"@microsoft/tsdoc": "^0.12.20",
"@rollup/plugin-virtual": "^2.0.3",
"@sewing-kit/cli": "^0.2.0",
"@sewing-kit/config": "^0.1.0",
"@sewing-kit/eslint-plugin": "^0.0.14",
Expand All @@ -45,9 +46,12 @@
"@types/uuid": "^8.3.0",
"lerna": "^3.22.1",
"markdown-table": "^2.0.0",
"node-fetch": "^2.6.1",
"nodemon": "^2.0.4",
"react": ">=17.0.0 <18.0.0",
"resolve": "^1.17.0",
"rollup": "^2.52.2",
"rollup-plugin-jsx": "^1.0.3",
"uuid": "^8.3.2"
},
"resolutions": {
Expand Down
20 changes: 13 additions & 7 deletions scripts/generate-docs-admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,26 @@ const paths = {
JavaScript: './packages/admin-ui-extensions',
React: './packages/admin-ui-extensions-react',
},
outputRoot: '../shopify-dev/content/apps/app-extensions/ui-extensions',
shopifyDevUrl: '/apps/app-extensions/ui-extensions',
outputRoot: '../shopify-dev/content/api/admin-extensions',
shopifyDevUrl: '/api/admin-extensions',
};

components(paths, {
title: 'Components for Admin UI Extensions',
frontMatterDescription: 'A list of components for Admin UI Extensions.',
description: `
components(
paths,
{
title: 'Components for Admin UI Extensions',
frontMatterDescription: 'A list of components for Admin UI Extensions.',
description: `
Each component has general guidelines for usage as well as additional information regarding the behavior on certain platforms.
- 📱 denotes mobile specific information
- 🖥 denotes desktop specific information
`,
});
},
{
compileExamples: true,
},
);

adminExtensionApi(paths, {
componentsToSkip: ['ContainerApi', 'DataApi'],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import {resolve, extname} from 'path';
import * as fs from 'fs';

import type {Paths, Packages} from '../types';
import type {Paths} from '../../types';

import {createDependencyGraph} from '../utilities/dependency-graph';
import {createDependencyGraph} from '../../utilities/dependency-graph';

import {
renderYamlFrontMatter,
Expand All @@ -14,10 +14,15 @@ import {
strip,
firstSentence,
mkdir,
renderExamples,
findExamplesFor,
} from './shared';
import type {Node, Visibility} from './shared';
} from '../shared';
import type {Node, Visibility} from '../shared';

import {
findExamplesForComponent,
renderExamplesForComponent,
renderSandboxComponentExamples,
compileComponentExamples,
} from './utilities';

export interface Content {
title: string;
Expand All @@ -29,6 +34,8 @@ interface Options {
subcomponentMap?: {[rootComponent: string]: string[]};
componentsToSkip?: string[];
generateReadmes?: boolean;
/** Compile examples using Rollup and output alongside example files */
compileExamples?: boolean;
visibility?: Visibility;
}

Expand All @@ -45,6 +52,7 @@ export async function components(
componentsToSkip = [],
generateReadmes = false,
visibility = 'hidden',
compileExamples = false,
} = options;

const visibilityFrontMatter = visibilityToFrontMatterMap.get(visibility);
Expand Down Expand Up @@ -94,9 +102,22 @@ export async function components(
markdown += renderExampleImageFor(name, paths.shopifyDevAssets);

// 2. Examples
const examples = findExamplesFor(name, paths.packages, '/components');
const examples = findExamplesForComponent(
name,
paths.packages,
'/components',
);

if (examples.size > 0) {
markdown += renderExamples(examples);
if (compileExamples === true) {
const examplesUrl = `/sandbox-examples/${filename}`;
const examplesPath = resolve(`../shopify-dev/public/${examplesUrl}`);

compileComponentExamples(examples, examplesPath);
markdown += renderSandboxComponentExamples(examples, examplesUrl);
} else {
markdown += renderExamplesForComponent(examples);
}
}

// 3. Props table
Expand Down
2 changes: 2 additions & 0 deletions scripts/typedoc/shopify-dev-renderer/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export {components} from './components';
export type {Content} from './components';
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import * as fs from 'fs';
import {resolve, extname} from 'path';
import {rollup, ModuleFormat} from 'rollup';
import virtual from '@rollup/plugin-virtual';
import jsx from 'rollup-plugin-jsx';

import {skypackUrlResolve} from './skypack-url-resolve';
import type {Example} from './types';

export function renderSandboxComponentExamples(
examples: Map<string, Example>,
compiledPath: string,
): string {
let markdown = '';

const compiledFilename = getCompiledFilename(examples);

markdown += `{% codeblock extensions_sandbox, compiled: "${compiledPath}/${compiledFilename}" %}\n\n`;

examples.forEach((example, key) => {
markdown += [
`{% code ${example.extension}, title: "${key}" %}`,
`${example.content}`,
'{% endcode %}',
'\n',
].join('\n');
});

markdown += '{% endcodeblock %}\n';

return markdown;
}

function getCompiledFilename(examples: Map<string, Example>): string {
// we assume the first item in paths.packages is the one we want to feed to the compiler
const key = [...examples.keys()][0];
const example = examples.get(key);
return example.filename.replace(extname(example.filename), '.js');
}

export function compileComponentExamples(
examples: Map<string, Example>,
examplesPath: string,
) {
if (!fs.existsSync(examplesPath)) {
fs.mkdirSync(examplesPath, {recursive: true});
}

// we actually only need one compiled file per example
examples.forEach(async (example, key) => {
if (key === 'React') return;

try {
const compiledContent = await compileForSandbox(example.content);

const compiledFilename = example.filename.replace(
extname(example.filename),
'.js',
);
fs.writeFile(
`${examplesPath}/${compiledFilename}`,
compiledContent,
function (err) {
if (err) throw err;
},
);
} catch (error) {
console.log(`error compiling ${example.filename}`);
console.log(error);
}
});
}

export async function compileForSandbox(inputCode: string): Promise<string> {
const LOCAL_ID = 'INPUT';
const virtualModules = {
[LOCAL_ID]: inputCode,
};

const inputOptions = {
input: LOCAL_ID,
plugins: [
// The virtual plugin let us supply a string instead of a file as input
virtual(virtualModules),
skypackUrlResolve(),
jsx({factory: 'React.createElement'}),
],
};

const outputOptions = {
format: 'esm' as ModuleFormat,
};

const bundle = await rollup(inputOptions);
const {output} = await bundle.generate(outputOptions);

const outputCode = `${output[0].code}`;
return outputCode;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import * as fs from 'fs';
import {resolve, extname} from 'path';

import type {Packages} from '../../../types';
import type {Example} from './types';

export function findExamplesForComponent(
componentName: string,
packages: Packages,
subPath: string,
): Map<string, Example> {
const examples = new Map();

Object.keys(packages).forEach((packageName) => {
const packagePath = packages[packageName];
const componentExamplesFolder = resolve(
`${packagePath}/src${subPath}/${componentName}/examples`,
);

if (fs.existsSync(componentExamplesFolder)) {
fs.readdirSync(componentExamplesFolder).forEach((file) => {
examples.set(packageName, {
filename: file,
extension: extname(file).split('.').pop(),
content: fs.readFileSync(
`${componentExamplesFolder}/${file}`,
'utf8',
),
});
});
}
});

return examples;
}

export function renderExamplesForComponent(
examples: Map<string, Example>,
): string {
if (examples.size === 0) {
return '';
}
let markdown = '';

markdown += `{% codeblock %}\n\n`;

examples.forEach((example, key) => {
markdown += [
`{% code ${example.extension}, title: "${key}" %}{% raw %}`,
`${example.content}`,
'{% endraw %}{% endcode %}',
'\n',
].join('\n');
});

markdown += '{% endcodeblock %}\n\n';

return markdown;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export {findExamplesForComponent, renderExamplesForComponent} from './examples';

export {
renderSandboxComponentExamples,
compileComponentExamples,
} from './compile-for-sandbox';
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import fetch from 'node-fetch';

const SKYPACK_URL = 'https://cdn.skypack.dev';

const pinnedUrls = new Map<string, string>();

export function skypackUrlResolve() {
return {
async resolveId(source: string) {
const convertedSource = await convertToSkypackSource(source);
const maybeAbsoluteUrl = convertSkypackPathsToAbsoluteUrls(
convertedSource,
);
const url = parseURL(maybeAbsoluteUrl);
return url && isValidURL(url) ? url.href : null;
},
async load(id: string) {
const url = parseURL(id);
const result = url && isValidURL(url) ? await loadURL(url) : null;
return result;
},
};
}

function parseURL(source: string): URL | null {
try {
return new URL(source);
} catch (error) {
return null;
}
}

function isValidURL(url: URL): boolean {
return url !== null && ['http:', 'https:'].indexOf(url.protocol) >= 0;
}

async function loadURL(url: URL) {
switch (url.protocol) {
case 'http:':
case 'https:':
return fetch(url.href).then((res) =>
res.status === 404 ? null : res.text(),
);
default:
throw new Error(`Cannot load URL protocol: ${url.protocol}`);
}
}

/** Initial req to skypack returns a 'pinned' URL which loads faster on subsequent reqs */
async function pinnedUrl(pkg: string) {
if (pinnedUrls.get(pkg) === undefined) {
const res = await fetch(`${SKYPACK_URL}/${pkg}`);
const importUrl = res.headers.get('x-import-url');
pinnedUrls.set(pkg, `${SKYPACK_URL}${importUrl}`);
}
return pinnedUrls.get(pkg);
}

async function convertToSkypackSource(pkg: string) {
switch (pkg) {
case '@shopify/admin-ui-extensions':
case '@shopify/admin-ui-extensions-react':
return pinnedUrl(`${pkg}`);
default:
return pkg;
}
}

function convertSkypackPathsToAbsoluteUrls(path: string): string {
return path.startsWith('/-/') || path.startsWith('/new/')
? `${SKYPACK_URL}${path}`
: path;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface Example {
filename: string;
extension: string;
content: string;
}
Loading

0 comments on commit 1b1361e

Please sign in to comment.