Skip to content
This repository has been archived by the owner on Oct 23, 2023. It is now read-only.

Commit

Permalink
feat: implement static html export (#414)
Browse files Browse the repository at this point in the history
  • Loading branch information
marionebl authored and tilmx committed May 7, 2018
1 parent 099ef8d commit 948111f
Show file tree
Hide file tree
Showing 16 changed files with 641 additions and 346 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/component/chrome/chrome-container.tsx
Expand Up @@ -29,7 +29,7 @@ export class ChromeContainer extends React.Component {

const currentPage = this.getCurrentPage();
const project = currentPage ? currentPage.getProject() : undefined;
const pages = project ? project.getPages() : [];
const pages = project ? project.getPageRefs() : [];
const currentIndex = currentPage ? pages.indexOf(currentPage.getPageRef()) : 0;

if (currentIndex > 0) {
Expand Down
2 changes: 1 addition & 1 deletion src/component/page-list/page-list-container.tsx
Expand Up @@ -21,7 +21,7 @@ export const PageListContainer: React.StatelessComponent = observer((): JSX.Elem
<Space sizeBottom={SpaceSize.XXXL * 3}>
<Layout wrap={LayoutWrap.Wrap}>
{project
.getPages()
.getPageRefs()
.map((pageRef: PageRef, i: number) => (
<PageTileContainer
focused={pageRef.getId() === currentPageId}
Expand Down
33 changes: 30 additions & 3 deletions src/electron/menu.ts
@@ -1,12 +1,14 @@
import { BrowserWindow, ipcRenderer, MenuItem, MenuItemConstructorOptions, remote } from 'electron';
import { ElementLocationCommand } from '../store/command/element-location-command';
import * as FsExtra from 'fs-extra';
import { HtmlExporter } from '../export/html-exporter';
import { Page } from '../store/page/page';
import { PageElement } from '../store/page/page-element';
import * as Path from 'path';
import { PdfExporter } from '../export/pdf-exporter';
import { PngExporter } from '../export/png-exporter';
import * as Process from 'process';
import { Project } from '../store/project';
import { SketchExporter } from '../export/sketch-exporter';
import { Store } from '../store/store';
const { Menu, shell, app, dialog } = remote;
Expand All @@ -15,6 +17,10 @@ function getPageFileName(): string {
return (Store.getInstance().getCurrentPage() as Page).getName();
}

function getProjectFileName(): string {
return (Store.getInstance().getCurrentProject() as Project).getName();
}

interface PathQuery {
defaultName: string;
extname: string;
Expand Down Expand Up @@ -113,7 +119,7 @@ export function createMenu(): void {
label: '&Export',
submenu: [
{
label: 'Export page as Sketch',
label: 'Export Page as Sketch',
enabled: !isSplashscreen,
click: async () => {
const path = await queryPath({
Expand All @@ -131,7 +137,7 @@ export function createMenu(): void {
}
},
{
label: 'Export page as PDF',
label: 'Export Page as PDF',
enabled: !isSplashscreen,
click: async () => {
const path = await queryPath({
Expand All @@ -149,7 +155,7 @@ export function createMenu(): void {
}
},
{
label: 'Export page as PNG',
label: 'Export Page as PNG',
enabled: !isSplashscreen,
click: async () => {
const path = await queryPath({
Expand All @@ -165,6 +171,27 @@ export function createMenu(): void {
await pngExporter.writeToDisk(path);
}
}
},
{
type: 'separator'
},
{
label: 'Export Project as HTML',
enabled: !isSplashscreen,
click: async () => {
const path = await queryPath({
title: 'Export HTML as',
typeName: 'HTML File',
defaultName: getProjectFileName(),
extname: 'html'
});

if (path) {
const htmlExporter = new HtmlExporter();
await htmlExporter.createExport();
await htmlExporter.writeToDisk(path);
}
}
}
]
},
Expand Down
10 changes: 7 additions & 3 deletions src/electron/renderer.ts
Expand Up @@ -42,12 +42,16 @@ MobX.autorun(() => {
});

MobX.autorun(() => {
const page = store.getCurrentPage();
const project = store.getCurrentProject();
const currentPage = store.getCurrentPage();

if (page) {
if (project && currentPage) {
ipcRenderer.send('message', {
type: 'page-change',
payload: page.toJsonObject({ forRendering: true })
payload: {
pageId: currentPage.getId(),
pages: project.getPages().map(page => page.toJsonObject({ forRendering: true }))
}
});
}
});
Expand Down
83 changes: 12 additions & 71 deletions src/electron/server.ts
@@ -1,38 +1,26 @@
import { createCompiler } from '../preview/create-compiler';
import { EventEmitter } from 'events';
import * as express from 'express';
import * as Http from 'http';
import { ServerMessageType } from '../message';
import * as Path from 'path';
import { patternIdToWebpackName } from '../preview/pattern-id-to-webpack-name';
import { previewDocument } from '../preview/preview-document';
import * as QueryString from 'query-string';
import { previewDocument, PreviewDocumentMode } from '../preview/preview-document';
import { Store } from '../store/store';
import { Styleguide } from '../store/styleguide/styleguide';
import * as uuid from 'uuid';
import * as webpack from 'webpack';
import { OPEN, Server as WebsocketServer } from 'ws';

// memory-fs typings on @types are faulty
const MemoryFs = require('memory-fs');

const PREVIEW_PATH = require.resolve('../preview/preview');
const LOADER_PATH = require.resolve('../preview/components-loader');
const RENDERER_PATH = require.resolve('../preview/preview-renderer');

export interface ServerOptions {
port: number;
}

interface StyleguidePattern {
[key: string]: string;
}

interface State {
id: string;
payload: {
elementId?: string;
// tslint:disable-next-line:no-any
page?: any;
// tslint:disable-next-line:no-any
pages?: any[];
};
type: 'state';
}
Expand Down Expand Up @@ -88,7 +76,12 @@ export async function createServer(opts: ServerOptions): Promise<EventEmitter> {

app.get('/preview.html', (req, res) => {
res.type('html');
res.send(previewDocument);
res.send(
previewDocument({
mode: PreviewDocumentMode.Live,
data: state
})
);
});

app.use('/scripts', (req, res, next) => {
Expand Down Expand Up @@ -174,7 +167,7 @@ export async function createServer(opts: ServerOptions): Promise<EventEmitter> {
break;
}
case ServerMessageType.PageChange: {
state.payload.page = message.payload;
state.payload = message.payload;
send(state);
break;
}
Expand Down Expand Up @@ -229,66 +222,14 @@ function startServer(options: ServerStartOptions): Promise<void> {
// tslint:disable-next-line:no-any
async function setup(update: any): Promise<any> {
const queue: Queue = [];
const init: StyleguidePattern = {};

const styleguide = new Styleguide(
update.styleguidePath,
update.patternsPath,
update.analyzerName
);

const context = styleguide.getPath();

const components = styleguide.getPatterns().reduce((componentMap, pattern) => {
const patternPath = pattern.getImplementationPath();

if (!patternPath) {
return componentMap;
}

componentMap[patternIdToWebpackName(pattern.getId())] = `./${Path.relative(
context,
patternPath
)
.split(Path.sep)
.join('/')}`;
return componentMap;
}, init);

const compiler = webpack({
mode: 'development',
context,
entry: {
components: `${LOADER_PATH}?${QueryString.stringify({
cwd: context,
components: JSON.stringify(components)
})}!`,
renderer: RENDERER_PATH,
preview: PREVIEW_PATH
},
output: {
filename: '[name].js',
library: '[name]',
libraryTarget: 'window',
path: '/'
},
optimization: {
splitChunks: {
cacheGroups: {
vendor: {
chunks: 'initial',
name: 'vendor',
test: /node_modules/,
priority: 10,
enforce: true
}
}
}
},
plugins: [new webpack.HotModuleReplacementPlugin()]
});

compiler.outputFileSystem = new MemoryFs();
const compiler = createCompiler(styleguide);

compiler.hooks.compile.tap('alva', () => {
queue.unshift({ type: WebpackMessageType.Start, id: uuid.v4() });
Expand Down
61 changes: 61 additions & 0 deletions src/export/html-exporter.ts
@@ -0,0 +1,61 @@
import { createCompiler } from '../preview/create-compiler';
import { Exporter, ExportResult } from './exporter';
import * as Fs from 'fs';
import * as Path from 'path';
import { previewDocument, PreviewDocumentMode } from '../preview/preview-document';
import { Store } from '../store/store';
import * as Util from 'util';
import * as uuid from 'uuid';

const SCRIPTS = ['vendor', 'renderer', 'components', 'preview'];

const createScript = (name: string, content: string) =>
`<script data-script="${name}">${content}<\/script>`;

export class HtmlExporter extends Exporter {
public async createExport(): Promise<ExportResult> {
const store = Store.getInstance();
const project = store.getCurrentProject();
const currentPage = store.getCurrentPage();
const styleguide = store.getStyleguide();

// TODO: Come up with good user-facing errors
if (!project || !currentPage || !styleguide) {
return {};
}

const data = {
id: uuid.v4(),
type: 'state',
payload: {
mode: PreviewDocumentMode.Static,
pageId: currentPage.getId(),
pages: project.getPages().map(page => page.toJsonObject({ forRendering: true }))
}
};

const compiler = createCompiler(styleguide);
await Util.promisify(compiler.run).bind(compiler)();

const fs = (await compiler.outputFileSystem) as typeof Fs;

const scripts = SCRIPTS.map(name => [
name,
fs.readFileSync(Path.posix.join('/', `${name}.js`)).toString()
])
.map(([name, content]) => createScript(name, content))
.join('\n');

const document = previewDocument({
data,
mode: PreviewDocumentMode.Static,
scripts
});

this.contents = Buffer.from(document);

return {
result: this.contents
};
}
}

0 comments on commit 948111f

Please sign in to comment.