Skip to content

Commit 17f80b0

Browse files
Added file writers to complete JSX/TSX deck generation. (#1145)
* Added file writers to complete JSX/TSX deck generation. * Added style loader * Ensure production flag is set on build. * Added default values for optional flags. * Use the workspace package version for Spectacle in the CLI * Lock file changes * Added carrot to spectacle version specifier
1 parent 81eb809 commit 17f80b0

File tree

11 files changed

+341
-92
lines changed

11 files changed

+341
-92
lines changed

packages/cli/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
},
2828
"peerDependencies": {},
2929
"devDependencies": {
30+
"spectacle": "workspace:spectacle@*",
3031
"@types/node": "^18.0.3",
3132
"nodemon": "^2.0.18",
3233
"rimraf": "^3.0.2",

packages/cli/src/cli.ts

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,17 @@ import { Command } from 'commander';
55
import cliSpinners from 'cli-spinners';
66
import logUpdate from 'log-update';
77
import {
8-
writeBaseWebpackProjectFiles,
8+
FileOptions,
9+
writeWebpackProjectFiles,
910
writeOnePageHTMLFile
10-
} from './templates';
11+
} from './templates/file-writers';
12+
import { version as SPECTACLE_VERSION } from 'spectacle/package.json';
1113

1214
type CLIOptions = {
1315
type: 'tsx' | 'jsx' | 'mdx' | 'onepage';
1416
name: string;
17+
lang?: string;
18+
port?: number;
1519
};
1620

1721
let progressInterval: NodeJS.Timer;
@@ -41,11 +45,15 @@ const main = async () => {
4145
'deck source type (choices: "tsx", "jsx", "mdx", "onepage")'
4246
)
4347
.requiredOption('-n, --name [name]', 'name of presentation')
48+
.option(
49+
'-l, --lang [lang]',
50+
'language code for generated HTML document, default: en'
51+
)
52+
.option('-p, --port [port]', 'port for webpack dev server, default: 3000')
4453
.parse(process.argv);
4554

4655
let i = 0;
47-
const { type, name } = program.opts<CLIOptions>();
48-
const snakeCaseName = name.toLowerCase().replace(/([^a-z0-9]+)/gi, '-');
56+
const { type, name, lang = 'en', port = 3000 } = program.opts<CLIOptions>();
4957

5058
progressInterval = setInterval(() => {
5159
const { frames } = cliSpinners.aesthetic;
@@ -57,24 +65,24 @@ const main = async () => {
5765

5866
await sleep(750);
5967

68+
const fileOptions: FileOptions = {
69+
snakeCaseName: name.toLowerCase().replace(/([^a-z0-9]+)/gi, '-'),
70+
name,
71+
lang,
72+
port,
73+
enableTypeScriptSupport: type === 'tsx',
74+
spectacleVersion: SPECTACLE_VERSION
75+
};
76+
6077
switch (type) {
6178
case 'jsx':
62-
await writeBaseWebpackProjectFiles({
63-
snakeCaseName,
64-
name
65-
});
79+
await writeWebpackProjectFiles(fileOptions);
6680
break;
6781
case 'tsx':
68-
await writeBaseWebpackProjectFiles({
69-
snakeCaseName,
70-
name
71-
});
82+
await writeWebpackProjectFiles(fileOptions);
7283
break;
7384
case 'onepage':
74-
await writeOnePageHTMLFile({
75-
snakeCaseName,
76-
name
77-
});
85+
await writeOnePageHTMLFile(fileOptions);
7886
break;
7987
}
8088

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
type BabelTemplateOptions = {
2+
enableTypeScriptSupport: boolean;
3+
};
4+
5+
export const babelTemplate = (options: BabelTemplateOptions) =>
6+
`{
7+
"presets": [
8+
${options.enableTypeScriptSupport ? '"@babel/preset-typescript",' : ''}
9+
["@babel/preset-env", { "modules": false }],
10+
["@babel/preset-react", { "runtime": "automatic" }]
11+
],
12+
"plugins": [
13+
"@babel/plugin-proposal-object-rest-spread",
14+
"@babel/plugin-proposal-class-properties"
15+
],
16+
"env": {
17+
"cjs": {
18+
"presets": ["@babel/preset-env", "@babel/preset-react"]
19+
},
20+
"test": {
21+
"presets": ["@babel/preset-env", "@babel/preset-react"]
22+
}
23+
}
24+
}
25+
`;
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import path from 'path';
2+
import { existsSync } from 'fs';
3+
import { mkdir, writeFile, rm } from 'fs/promises';
4+
import { htmlTemplate } from './html';
5+
import { onePageTemplate } from './one-page';
6+
import { webpackTemplate } from './webpack';
7+
import { babelTemplate } from './babel';
8+
import { packageTemplate } from './package';
9+
import { indexTemplate } from './index';
10+
import { tsconfigTemplate } from './tsconfig';
11+
12+
export type FileOptions = {
13+
snakeCaseName: string;
14+
name: string;
15+
lang: string;
16+
port: number;
17+
enableTypeScriptSupport: boolean;
18+
spectacleVersion: string;
19+
};
20+
21+
export const writeWebpackProjectFiles = async ({
22+
snakeCaseName,
23+
name,
24+
lang,
25+
port,
26+
enableTypeScriptSupport,
27+
spectacleVersion
28+
}: FileOptions) => {
29+
const outPath = path.resolve(process.cwd(), snakeCaseName);
30+
31+
await rm(outPath, { recursive: true, force: true });
32+
33+
if (existsSync(outPath)) {
34+
throw new Error(`Directory named ${snakeCaseName} already exists.`);
35+
}
36+
await mkdir(outPath, { recursive: true });
37+
await writeFile(`${snakeCaseName}/index.html`, htmlTemplate({ name, lang }));
38+
await writeFile(
39+
`${snakeCaseName}/webpack.config.js`,
40+
webpackTemplate({ port, usesTypeScript: enableTypeScriptSupport })
41+
);
42+
await writeFile(
43+
`${snakeCaseName}/.babelrc`,
44+
babelTemplate({ enableTypeScriptSupport })
45+
);
46+
await writeFile(
47+
`${snakeCaseName}/package.json`,
48+
packageTemplate({
49+
usesTypeScript: enableTypeScriptSupport,
50+
name: snakeCaseName,
51+
spectacleVersion
52+
})
53+
);
54+
await writeFile(
55+
`${snakeCaseName}/index.${enableTypeScriptSupport ? 'tsx' : 'jsx'}`,
56+
indexTemplate({
57+
usesTypeScript: enableTypeScriptSupport,
58+
name
59+
})
60+
);
61+
62+
enableTypeScriptSupport &&
63+
(await writeFile(`${snakeCaseName}/tsconfig.json`, tsconfigTemplate()));
64+
};
65+
66+
export const writeOnePageHTMLFile = async ({
67+
snakeCaseName,
68+
name,
69+
lang
70+
}: FileOptions) => {
71+
await writeFile(`${snakeCaseName}.html`, onePageTemplate({ name, lang }));
72+
};

packages/cli/src/templates/html.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
type HTMLTemplateOptions = {
22
name: string;
3+
lang: string;
34
};
45

56
export const htmlTemplate = (options: HTMLTemplateOptions) => `
67
<!DOCTYPE html>
7-
<html>
8+
<html lang="${options.lang}">
89
<head>
910
<title>${options.name}</title>
1011
</head>
Lines changed: 42 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,46 @@
1-
import path from 'path';
2-
import { existsSync } from 'fs';
3-
import { mkdir, writeFile } from 'fs/promises';
4-
import { htmlTemplate } from './html';
5-
import { onePageTemplate } from './one-page';
6-
7-
type FileOptions = {
8-
snakeCaseName: string;
1+
type IndexTemplateOptions = {
92
name: string;
3+
usesTypeScript: boolean;
104
};
115

12-
export const writeBaseWebpackProjectFiles = async ({
13-
snakeCaseName,
14-
name
15-
}: FileOptions) => {
16-
const outPath = path.resolve(process.cwd(), snakeCaseName);
17-
if (existsSync(outPath)) {
18-
throw new Error(`Directory named ${snakeCaseName} already exists.`);
19-
}
20-
await mkdir(outPath, { recursive: true });
21-
await writeFile(`${snakeCaseName}/index.html`, htmlTemplate({ name }));
22-
};
6+
export const indexTemplate = (options: IndexTemplateOptions) =>
7+
`import React from 'react';
8+
import { createRoot } from 'react-dom/client';
9+
import { Slide, Deck, FlexBox, Heading, SpectacleLogo, Box, FullScreen, AnimatedProgress } from 'spectacle';
2310
24-
export const writeOnePageHTMLFile = async ({
25-
snakeCaseName,
26-
name
27-
}: FileOptions) => {
28-
await writeFile(`${snakeCaseName}.html`, onePageTemplate({ name }));
29-
};
11+
const template = () => (
12+
<FlexBox
13+
justifyContent="space-between"
14+
position="absolute"
15+
bottom={0}
16+
width={1}
17+
>
18+
<Box padding="0 1em">
19+
<FullScreen />
20+
</Box>
21+
<Box padding="1em">
22+
<AnimatedProgress />
23+
</Box>
24+
</FlexBox>
25+
);
26+
27+
const Presentation = () => (
28+
<Deck template={template}>
29+
<Slide>
30+
<FlexBox height="100%">
31+
<Heading>${options.name}</Heading>
32+
</FlexBox>
33+
</Slide>
34+
<Slide>
35+
<FlexBox height="100%">
36+
<Heading fontSize="h2">Made with</Heading>
37+
<SpectacleLogo size={300} />
38+
</FlexBox>
39+
</Slide>
40+
</Deck>
41+
);
42+
43+
createRoot(document.getElementById('app')${
44+
options.usesTypeScript ? '!' : ''
45+
}).render(<Presentation />);
46+
`;

packages/cli/src/templates/one-page.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
type OnePageTemplateOptions = {
22
name: string;
3+
lang: string;
34
};
45

5-
export const onePageTemplate = ({ name }: OnePageTemplateOptions) => `
6+
export const onePageTemplate = ({ name, lang }: OnePageTemplateOptions) => `
67
<!DOCTYPE html>
7-
<html lang="en">
8+
<html lang="${lang}">
89
<head>
910
<meta charset="UTF-8" />
1011
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
type PackageTemplateOptions = {
2+
name: string;
3+
spectacleVersion: string;
4+
usesTypeScript: boolean;
5+
};
6+
7+
export const packageTemplate = (options: PackageTemplateOptions) =>
8+
`{
9+
"name": "${options.name}",
10+
"private": true,
11+
"scripts": {
12+
"start": "webpack-dev-server --hot --config ./webpack.config.js",
13+
"clean": "rimraf dist",
14+
"build": "webpack --config ./webpack.config.js --mode production"
15+
},
16+
"dependencies": {
17+
"spectacle": "^${options.spectacleVersion}",
18+
"react": "^18.1.0",
19+
"react-dom": "^18.1.0"
20+
},
21+
"devDependencies": {
22+
"@babel/core": "^7.17.2",
23+
"@babel/plugin-proposal-class-properties": "^7.12.1",
24+
"@babel/plugin-proposal-object-rest-spread": "^7.12.1",
25+
"@babel/preset-env": "^7.12.7",
26+
"@babel/preset-react": "^7.16.7",
27+
"babel-loader": "^8.0.6",
28+
"html-webpack-plugin": "^5.3.1",
29+
"style-loader": "^3.3.1",
30+
"css-loader": "^5.1.3",
31+
"file-loader": "^6.2.0",
32+
"rimraf": "^3.0.0",
33+
"webpack": "^5.68.0",
34+
"webpack-cli": "^4.5.0",
35+
"webpack-dev-server": "^4.7.4"${
36+
options.usesTypeScript
37+
? ',\n "typescript": "^4.5.2",' +
38+
'\n "@babel/preset-typescript": "^7.16.0",' +
39+
'\n "@types/react": "^18.0.12",' +
40+
'\n "@types/react-dom": "^18.0.5"'
41+
: ''
42+
}
43+
}
44+
}
45+
`;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
export const tsconfigTemplate = () =>
2+
`{
3+
"compilerOptions": {
4+
"target": "ES6",
5+
"lib": [
6+
"DOM",
7+
"ES2019"
8+
],
9+
"jsx": "react-jsx",
10+
"module": "commonjs",
11+
"moduleResolution": "node",
12+
"allowUmdGlobalAccess": true,
13+
"allowSyntheticDefaultImports": true,
14+
"esModuleInterop": true,
15+
"forceConsistentCasingInFileNames": true,
16+
"strict": true,
17+
"skipLibCheck": true
18+
}
19+
}
20+
`;
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
type WebpackTemplateOptions = {
2+
port: number;
3+
usesTypeScript: boolean;
4+
};
5+
6+
export const webpackTemplate = (options: WebpackTemplateOptions) =>
7+
`const path = require('path');
8+
const HtmlWebpackPlugin = require('html-webpack-plugin');
9+
10+
module.exports = {
11+
mode: 'development',
12+
context: __dirname,
13+
entry: './index.${options.usesTypeScript ? 'tsx' : 'jsx'}',
14+
output: {
15+
path: path.join(__dirname, '/dist'),
16+
filename: 'app.bundle.js'
17+
},
18+
devServer: {
19+
port: ${options.port}
20+
},
21+
module: {
22+
rules: [
23+
{ test: /\\.[tj]sx?$/, use: ['babel-loader'] },
24+
{ test: /\\.(png|svg|jpg|gif)$/, use: ['file-loader'] },
25+
{ test: /\\.css$/, use: ['style-loader', 'css-loader'] }
26+
]
27+
},
28+
plugins: [
29+
new HtmlWebpackPlugin({ template: './index.html' }),
30+
]
31+
};
32+
`;

0 commit comments

Comments
 (0)