diff --git a/.gitignore b/.gitignore
index 253be23..44a0d26 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,4 @@ node_modules/
package-lock.json
ci/temp
bokeh-bokehjs-*.tgz
+dist/
diff --git a/ci/typescript/create_vanilla_rspack.sh b/ci/typescript/create_vanilla_rspack.sh
index 1a61846..b96f206 100755
--- a/ci/typescript/create_vanilla_rspack.sh
+++ b/ci/typescript/create_vanilla_rspack.sh
@@ -60,8 +60,8 @@ const config: Configuration = {
export default config;
EOF
-# 5. Create HTML file
-mkdir assets
+# 5. Create HTML file assets/index.html
+mkdir -p assets
cat > assets/index.html << EOF
@@ -75,13 +75,13 @@ cat > assets/index.html << EOF
EOF
-# 6. Create source typescript file
-mkdir src
+# 6. Create source typescript file src/index.ts
+mkdir -p src
cat > src/index.ts << EOF
console.log("Successfully loaded")
EOF
-# 7. Add build and serve commands to package.json
+# 7. Add build and serve commands to the scripts section of package.json
cat > temp.json << EOF
{
"scripts": {
@@ -97,11 +97,13 @@ rm temp.json
# npm install
# npm run build
# npm run serve
+# In a web browser navigate to http://localhost:4500/
-# 9. Add BokehJS dependency
+# 9. Add BokehJS dependency to this project. This assumes the package has been built and copied to the root directory of this repository as outlined in the top-level README.md.
npm install ../../../../bokeh-bokehjs-3.7.0-dev.5.tgz
-# 10. Replace src/index.ts with code to create BokehJS plot
+# 10. Replace contents of src/index.ts with code to create BokehJS plot
+mkdir -p src
cat > src/index.ts << EOF
import * as Bokeh from "@bokeh/bokehjs";
@@ -137,4 +139,5 @@ EOF
# 11. Rebuild and serve
npm install
npm run build
-#npm run serve
+# npm run serve
+# In a web browser navigate to http://localhost:4500/
diff --git a/ci/typescript/create_vanilla_webpack.sh b/ci/typescript/create_vanilla_webpack.sh
index e62e838..de00768 100755
--- a/ci/typescript/create_vanilla_webpack.sh
+++ b/ci/typescript/create_vanilla_webpack.sh
@@ -61,8 +61,8 @@ const config: webpack.Configuration = {
export default config;
EOF
-# 5. Create HTML file
-mkdir assets
+# 5. Create HTML file assets/index.html
+mkdir -p assets
cat > assets/index.html << EOF
@@ -76,13 +76,13 @@ cat > assets/index.html << EOF
EOF
-# 6. Create source typescript file
-mkdir src
+# 6. Create source typescript file src/index.ts
+mkdir -p src
cat > src/index.ts << EOF
console.log("Successfully loaded")
EOF
-# 7. Add build and serve commands to package.json
+# 7. Add build and serve commands to the scripts section of package.json
cat > temp.json << EOF
{
"scripts": {
@@ -98,11 +98,13 @@ rm temp.json
# npm install
# npm run build
# npm run serve
+# In a web browser navigate to http://localhost:4500/
-# 9. Add BokehJS dependency
+# 9. Add BokehJS dependency to this project. This assumes the package has been built and copied to the root directory of this repository as outlined in the top-level README.md.
npm install ../../../../bokeh-bokehjs-3.7.0-dev.5.tgz
-# 10. Replace src/index.ts with code to create BokehJS plot
+# 10. Replace contents of src/index.ts with code to create BokehJS plot
+mkdir -p src
cat > src/index.ts << EOF
import * as Bokeh from "@bokeh/bokehjs";
@@ -138,4 +140,5 @@ EOF
# 11. Rebuild and serve
npm install
npm run build
-#npm run serve
+# npm run serve
+# In a web browser navigate to http://localhost:4500/
diff --git a/recipes/README.md b/recipes/README.md
new file mode 100644
index 0000000..afd0d79
--- /dev/null
+++ b/recipes/README.md
@@ -0,0 +1,12 @@
+Code to create recipes. Each recipe is defined in a TypeScript class and can be written to both a
+README markdown file for humans to follow, and a `bash` script that can be used to automatically
+create the recipe.
+
+To recreate all recipes:
+```bash
+npm install
+npm run build
+npm run create
+```
+
+This will overwrite all existing recipes. If you are happy with the changes, `git commit` them.
diff --git a/recipes/package.json b/recipes/package.json
new file mode 100644
index 0000000..b349255
--- /dev/null
+++ b/recipes/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "recipes",
+ "version": "1.0.0",
+ "license": "BSD-3-Clause",
+ "description": "Automated creation of recipe scripts and readme files",
+ "main": "index.js",
+ "types": "lib/index.d.ts",
+ "scripts": {
+ "build": "tsc",
+ "create": "node dist/runner.js"
+ },
+ "devDependencies": {
+ "@types/node": "^22.13.1",
+ "typescript": "^5.7.3"
+ }
+}
diff --git a/recipes/src/bash_writer.ts b/recipes/src/bash_writer.ts
new file mode 100644
index 0000000..f38d3c8
--- /dev/null
+++ b/recipes/src/bash_writer.ts
@@ -0,0 +1,36 @@
+import * as fs from 'node:fs';
+import * as path from 'node:path';
+
+import { Recipe } from './recipe';
+import { Step } from './step';
+import { Writer } from './writer';
+
+export class BashWriter extends Writer {
+ filename(recipe: Recipe): string {
+ return path.join(
+ '..', 'ci', recipe.type, 'create_' + recipe.framework + '_' + recipe.bundler + '.sh');
+ }
+
+ protected writeStep(fd: number, index: number, step: Step): void {
+ step.writeToBash(fd, index);
+ }
+
+ protected writePreable(fd: number, recipe: Recipe): void {
+ fs.writeSync(fd, `#!/usr/bin/env bash
+
+set -eux
+
+export OUTPUT_DIRECTORY=../temp/${recipe.type}/${recipe.framework}_${recipe.bundler}
+
+mkdir -p $OUTPUT_DIRECTORY
+cd $OUTPUT_DIRECTORY
+rm -rf *
+
+function merge-json() {
+ # merge the second json file into the first.
+ TEMP_FILE=$(mktemp)
+ jq '. * input' $1 $2 > TEMP_FILE && mv TEMP_FILE $1
+}
+`);
+ }
+}
diff --git a/recipes/src/index.ts b/recipes/src/index.ts
new file mode 100644
index 0000000..6ec1288
--- /dev/null
+++ b/recipes/src/index.ts
@@ -0,0 +1,4 @@
+export * from './bash_writer';
+export * from './readme_writer';
+export * from './recipe';
+export * from './writer';
diff --git a/recipes/src/readme_writer.ts b/recipes/src/readme_writer.ts
new file mode 100644
index 0000000..2ddb0ac
--- /dev/null
+++ b/recipes/src/readme_writer.ts
@@ -0,0 +1,30 @@
+import * as fs from 'node:fs';
+import * as path from 'node:path';
+
+import { Recipe } from './recipe';
+import { Step } from './step';
+import { Writer } from './writer';
+
+export class ReadmeWriter extends Writer {
+ filename(recipe: Recipe): string {
+ return path.join('..', recipe.type, recipe.framework + '_' + recipe.bundler, 'README.md');
+ }
+
+ protected writeStep(fd: number, index: number, step: Step): void {
+ step.writeToReadme(fd, index);
+ }
+
+ protected writePreable(fd: number, recipe: Recipe): void {
+ const { type, bundler } = recipe;
+
+ let { details, framework } = recipe;
+ const prefix = framework === 'vanilla' ? ' (no framework)' : '';
+ framework = framework.charAt(0).toUpperCase() + framework.slice(1);
+
+ fs.writeSync(fd, `# ${framework}${prefix} ${bundler} ${type} example\n`);
+
+ if (details) {
+ fs.writeSync(fd, '\n' + details + '\n');
+ }
+ }
+}
diff --git a/recipes/src/recipe.ts b/recipes/src/recipe.ts
new file mode 100644
index 0000000..a3de274
--- /dev/null
+++ b/recipes/src/recipe.ts
@@ -0,0 +1,23 @@
+import { Step } from './step';
+
+/**
+ * Abstract base class for recipe for making a BokehJS example, consisting of multiple steps.
+ */
+export abstract class Recipe {
+ constructor(
+ readonly type: string,
+ readonly framework: string,
+ readonly bundler: string,
+ readonly details: string = ''
+ ) {}
+
+ protected add(step: Step): void {
+ this._steps.push(step);
+ }
+
+ get steps(): Step[] {
+ return this._steps;
+ }
+
+ private _steps: Step[] = [];
+}
diff --git a/recipes/src/recipes/index.ts b/recipes/src/recipes/index.ts
new file mode 100644
index 0000000..83d3207
--- /dev/null
+++ b/recipes/src/recipes/index.ts
@@ -0,0 +1 @@
+export * from './typescript';
diff --git a/recipes/src/recipes/typescript/common.ts b/recipes/src/recipes/typescript/common.ts
new file mode 100644
index 0000000..4d9c087
--- /dev/null
+++ b/recipes/src/recipes/typescript/common.ts
@@ -0,0 +1,41 @@
+export const baseTSConfig =
+`{
+ "compilerOptions": {
+ "baseUrl": ".",
+ "esModuleInterop": true,
+ "moduleResolution": "node",
+ "outDir": "./dist",
+ "rootDir": "./src",
+ "target": "ES2022"
+ },
+ "include": ["src"]
+}`;
+
+export const baseTypeScriptExample =
+`import * as Bokeh from "@bokeh/bokehjs";
+
+console.info("BokehJS version:", Bokeh.version);
+
+function create_bokehjs_plot(target_id: string) {
+ const source = new Bokeh.ColumnDataSource({data: { x: [0.1, 0.9], y: [0.1, 0.9], size: [40, 10] }});
+
+ const plot = Bokeh.Plotting.figure({
+ title: "Example BokehJS plot", height: 500, width: 500,
+ x_range: [0, 1], y_range: [0, 1], sizing_mode: "stretch_width",
+ });
+
+ plot.scatter({ field: "x" }, { field: "y" }, {source, size: { field: "size" }});
+
+ const button = new Bokeh.Widgets.Button({label: "Click me to add a point", button_type: "primary"});
+ function button_callback() {
+ const data = source.data as any;
+ data.x.push(Math.random());
+ data.y.push(Math.random());
+ data.size.push(10 + Math.random()*30);
+ source.change.emit();
+ }
+ button.on_click(button_callback);
+
+ const column = new Bokeh.Column({children: [plot, button], sizing_mode: "stretch_width"});
+ Bokeh.Plotting.show(column, target_id);
+}`;
diff --git a/recipes/src/recipes/typescript/index.ts b/recipes/src/recipes/typescript/index.ts
new file mode 100644
index 0000000..a1a2bb9
--- /dev/null
+++ b/recipes/src/recipes/typescript/index.ts
@@ -0,0 +1,2 @@
+export * from './vanilla_rspack_recipe';
+export * from './vanilla_webpack_recipe';
diff --git a/recipes/src/recipes/typescript/vanilla_rspack_recipe.ts b/recipes/src/recipes/typescript/vanilla_rspack_recipe.ts
new file mode 100644
index 0000000..88f04bb
--- /dev/null
+++ b/recipes/src/recipes/typescript/vanilla_rspack_recipe.ts
@@ -0,0 +1,114 @@
+import { Recipe } from '../../recipe';
+import { CommandStep, CreateFileStep, MergeJsonStep } from '../../step';
+import { baseTSConfig, baseTypeScriptExample } from './common';
+
+export class VanillaRspackRecipe extends Recipe {
+ constructor() {
+ super(
+ 'typescript',
+ 'vanilla',
+ 'rspack',
+ 'This is almost identical to the vanilla webpack example, as `rspack` is designed to be a ' +
+ 'drop-in replacement for `webpack`.'
+ );
+
+ this.add(new CommandStep(
+ 'Create initial `package.json` (`npm` project settings)',
+ ['npm init --yes']
+ ));
+
+ this.add(new CommandStep(
+ 'Install dev dependencies',
+ ['npm install --save-dev typescript @rspack/core @rspack/cli ts-node ts-loader']
+ ));
+
+ this.add(new CreateFileStep(
+ 'Create typescript configuration `tsconfig.json`',
+ 'tsconfig.json',
+ baseTSConfig
+ ));
+
+ this.add(new CreateFileStep(
+ 'Create rspack configuration `rspack.config.ts`',
+ 'rspack.config.ts',
+`import path from 'path';
+import { Configuration } from '@rspack/cli';
+
+const config: Configuration = {
+ entry: './src/index.ts',
+ mode: 'development',
+ module: {
+ rules: [
+ { test: /\\.ts/, use: "ts-loader", exclude: /node_modules/ }
+ ],
+ },
+ output: { filename: 'bundle.js' },
+ devServer: {
+ static: {
+ directory: path.join(__dirname, 'assets'),
+ },
+ port: 4500,
+ },
+};
+
+export default config;`)
+ );
+
+ this.add(new CreateFileStep(
+ 'Create HTML file `assets/index.html`',
+ 'assets/index.html',
+`
+
+
+ BokehJS example: typescript vanilla rspack
+
+
+
+
+
+`)
+ );
+
+ this.add(new CreateFileStep(
+ 'Create source typescript file `src/index.ts`',
+ 'src/index.ts',
+ 'console.log("Successfully loaded")'
+ ));
+
+ this.add(new MergeJsonStep(
+ 'Add `build` and `serve` commands to the `scripts` section of `package.json`',
+ 'package.json',
+`{
+ "scripts": {
+ "build": "rspack build",
+ "serve": "rspack serve"
+ }
+}`)
+ );
+
+ this.add(new CommandStep(
+ 'Build and run basic example without any BokehJS',
+ ['npm install', 'npm run build', 'npm run serve'],
+ 'In a web browser navigate to http://localhost:4500/',
+ true
+ ));
+
+ this.add(new CommandStep(
+ 'Add BokehJS dependency to this project. This assumes the package has been built and ' +
+ 'copied to the root directory of this repository as outlined in the top-level `README.md`.',
+ ['npm install ../../../../bokeh-bokehjs-3.7.0-dev.5.tgz']
+ ));
+
+ this.add(new CreateFileStep(
+ 'Replace contents of `src/index.ts` with code to create BokehJS plot',
+ 'src/index.ts',
+ baseTypeScriptExample + '\n\ncreate_bokehjs_plot("#target");'
+ ));
+
+ this.add(new CommandStep(
+ 'Rebuild and serve',
+ ['npm install', 'npm run build', 'npm run serve'],
+ 'In a web browser navigate to http://localhost:4500/'
+ ));
+ }
+}
diff --git a/recipes/src/recipes/typescript/vanilla_webpack_recipe.ts b/recipes/src/recipes/typescript/vanilla_webpack_recipe.ts
new file mode 100644
index 0000000..9faeec8
--- /dev/null
+++ b/recipes/src/recipes/typescript/vanilla_webpack_recipe.ts
@@ -0,0 +1,113 @@
+import { Recipe } from '../../recipe';
+import { CommandStep, CreateFileStep, MergeJsonStep } from '../../step';
+import { baseTSConfig, baseTypeScriptExample } from './common';
+
+export class VanillaWebpackRecipe extends Recipe {
+ constructor() {
+ super(
+ 'typescript',
+ 'vanilla',
+ 'webpack',
+ );
+
+ this.add(new CommandStep(
+ 'Create initial `package.json` (`npm` project settings)',
+ ['npm init --yes']
+ ));
+
+ this.add(new CommandStep(
+ 'Install dev dependencies',
+ ['npm install --save-dev typescript webpack webpack-cli webpack-dev-server ts-node ts-loader']
+ ));
+
+ this.add(new CreateFileStep(
+ 'Create typescript configuration `tsconfig.json`',
+ 'tsconfig.json',
+ baseTSConfig
+ ));
+
+ this.add(new CreateFileStep(
+ 'Create webpack configuration `webpack.config.ts`',
+ 'webpack.config.ts',
+`import path from 'path';
+import webpack from 'webpack';
+import 'webpack-dev-server';
+
+const config: webpack.Configuration = {
+ entry: './src/index.ts',
+ mode: 'development',
+ module: {
+ rules: [
+ { test: /\\.ts/, use: "ts-loader", exclude: /node_modules/ }
+ ],
+ },
+ output: { filename: 'bundle.js' },
+ devServer: {
+ static: {
+ directory: path.join(__dirname, 'assets'),
+ },
+ port: 4500,
+ },
+};
+
+export default config;`)
+ );
+
+ this.add(new CreateFileStep(
+ 'Create HTML file `assets/index.html`',
+ 'assets/index.html',
+`
+
+
+ BokehJS example: typescript vanilla webpack
+
+
+
+
+
+`)
+ );
+
+ this.add(new CreateFileStep(
+ 'Create source typescript file `src/index.ts`',
+ 'src/index.ts',
+ 'console.log("Successfully loaded")'
+ ));
+
+ this.add(new MergeJsonStep(
+ 'Add `build` and `serve` commands to the `scripts` section of `package.json`',
+ 'package.json',
+`{
+ "scripts": {
+ "build": "webpack build",
+ "serve": "webpack serve"
+ }
+}`)
+ );
+
+ this.add(new CommandStep(
+ 'Build and run basic example without any BokehJS',
+ ['npm install', 'npm run build', 'npm run serve'],
+ 'In a web browser navigate to http://localhost:4500/',
+ true
+ ));
+
+ this.add(new CommandStep(
+ 'Add BokehJS dependency to this project. This assumes the package has been built and ' +
+ 'copied to the root directory of this repository as outlined in the top-level `README.md`.',
+ ['npm install ../../../../bokeh-bokehjs-3.7.0-dev.5.tgz']
+ ));
+
+ this.add(new CreateFileStep(
+ 'Replace contents of `src/index.ts` with code to create BokehJS plot',
+ 'src/index.ts',
+ baseTypeScriptExample + '\n\ncreate_bokehjs_plot("#target");'
+ ));
+
+ this.add(new CommandStep(
+ 'Rebuild and serve',
+ ['npm install', 'npm run build', 'npm run serve'],
+ 'In a web browser navigate to http://localhost:4500/'
+ ));
+ }
+}
diff --git a/recipes/src/runner.ts b/recipes/src/runner.ts
new file mode 100644
index 0000000..fb84d8a
--- /dev/null
+++ b/recipes/src/runner.ts
@@ -0,0 +1,14 @@
+import { BashWriter, ReadmeWriter, Recipe, Writer } from '.';
+import * as allRecipes from './recipes/typescript';
+
+const writers: Writer[] = [new BashWriter(), new ReadmeWriter()];
+
+for (const cls of Object.values(allRecipes)) {
+ const recipe: Recipe = new (cls as any)();
+ console.log(`Recipe ${recipe.type} ${recipe.framework} ${recipe.bundler}`);
+
+ for (const writer of writers) {
+ console.log(` Writing to ${writer.filename(recipe)}`);
+ writer.write(recipe);
+ }
+}
diff --git a/recipes/src/step.ts b/recipes/src/step.ts
new file mode 100644
index 0000000..de2fa69
--- /dev/null
+++ b/recipes/src/step.ts
@@ -0,0 +1,147 @@
+import * as fs from 'node:fs';
+import * as path from 'node:path';
+
+import { fileExtension, languageFromExtension, removeBackticks } from './util';
+
+/**
+ * Single step of a recipe.
+ */
+export abstract class Step { //} implements IWriteVisitor {
+ constructor(readonly description: string) {}
+
+ abstract writeToBash(fd: number, index: number): void;
+
+ abstract writeToReadme(fd: number, index: number): void;
+
+ protected writeDescriptionBash(fd: number, index: number, suffix: string = ''): void {
+ fs.writeSync(fd, `\n# ${index+1}. ${removeBackticks(this.description)}${suffix}\n`);
+ }
+
+ protected writeDescriptionReadme(fd: number, index: number, suffix: string = ''): void {
+ fs.writeSync(fd, `\n${index+1}. ${this.description}${suffix}\n`);
+ }
+
+ protected spacer = ' '; // Spacer for indented lines in README.
+}
+
+/**
+ * Step consisting of one or more shell commands.
+ */
+export class CommandStep extends Step {
+ constructor(
+ readonly description: string,
+ readonly commands: string[],
+ readonly postscript: string = '',
+ readonly ignoreIfBash: boolean = false
+ ) {
+ super(description);
+ }
+
+ writeToBash(fd: number, index: number): void {
+ this.writeDescriptionBash(fd, index);
+
+ const allPrefix = this.ignoreIfBash ? '# ' : '';
+ for (const command of this.commands) {
+ const prefix = command === 'npm run serve' ? '# ' : allPrefix;
+ fs.writeSync(fd, prefix + command + '\n');
+ }
+ if (this.postscript) {
+ fs.writeSync(fd, '# ' + this.postscript + '\n');
+ }
+ }
+
+ writeToReadme(fd: number, index: number): void {
+ this.writeDescriptionReadme(fd, index);
+
+ const { spacer } = this;
+ fs.writeSync(fd, '\n' + spacer + '```bash\n');
+ for (const command of this.commands) {
+ fs.writeSync(fd, spacer + command + '\n');
+ }
+ fs.writeSync(fd, spacer + '```\n');
+
+ if (this.postscript) {
+ fs.writeSync(fd, '\n' + spacer + this.postscript + '\n');
+ }
+ }
+}
+
+/**
+ * Step to create a file.
+ */
+export class CreateFileStep extends Step {
+ constructor(readonly description: string, readonly filename: string, readonly contents: string) {
+ super(description);
+ }
+
+ writeToBash(fd: number, index: number): void {
+ this.writeDescriptionBash(fd, index);
+
+ const dirname = path.dirname(this.filename);
+ if (dirname !== '.') {
+ fs.writeSync(fd, `mkdir -p ${dirname}\n`);
+ }
+
+ fs.writeSync(fd, `cat > ${this.filename} << EOF\n`);
+ fs.writeSync(fd, this.contents);
+ if (this.contents.at(-1) !== '\n') {
+ fs.writeSync(fd, '\n');
+ }
+ fs.writeSync(fd, 'EOF\n');
+ }
+
+ writeToReadme(fd: number, index: number): void {
+ this.writeDescriptionReadme(fd, index, ' containing');
+
+ const { spacer } = this;
+ const language = languageFromExtension(this.filename);
+ fs.writeSync(fd, '\n' + spacer + '```' + language + '\n');
+ for (const line of this.contents.split('\n')) {
+ if (line) {
+ fs.writeSync(fd, spacer + line + '\n');
+ } else {
+ fs.writeSync(fd, '\n');
+ }
+ }
+ fs.writeSync(fd, spacer + '```\n');
+ }
+}
+
+/**
+ * Step to create a file.
+ */
+export class MergeJsonStep extends Step {
+ constructor(readonly description: string, readonly filename: string, readonly toMerge: string) {
+ super(description);
+ }
+
+ writeToBash(fd: number, index: number): void {
+ this.writeDescriptionBash(fd, index);
+
+ const tempFilename = 'temp' + fileExtension(this.filename);
+ fs.writeSync(fd, `cat > ${tempFilename} << EOF\n`);
+ fs.writeSync(fd, this.toMerge);
+ if (this.toMerge.at(-1) !== '\n') {
+ fs.writeSync(fd, '\n');
+ }
+ fs.writeSync(fd, 'EOF\n');
+ fs.writeSync(fd, `merge-json ${this.filename} ${tempFilename}\n`);
+ fs.writeSync(fd, `rm ${tempFilename}\n`);
+ }
+
+ writeToReadme(fd: number, index: number): void {
+ this.writeDescriptionReadme(fd, index);
+
+ const { spacer } = this;
+ const language = languageFromExtension(this.filename);
+ fs.writeSync(fd, '\n' + spacer + '```' + language + '\n');
+ for (const line of this.toMerge.split('\n')) {
+ if (line) {
+ fs.writeSync(fd, spacer + line + '\n');
+ } else {
+ fs.writeSync(fd, '\n');
+ }
+ }
+ fs.writeSync(fd, spacer + '```\n');
+ }
+}
diff --git a/recipes/src/util.ts b/recipes/src/util.ts
new file mode 100644
index 0000000..f9b9149
--- /dev/null
+++ b/recipes/src/util.ts
@@ -0,0 +1,28 @@
+import * as path from 'node:path';
+
+export function fileExtension(filename: string): string {
+ const extension = path.extname(filename);
+ return extension === '.' ? '' : extension;
+}
+
+export function languageFromExtension(filename: string): string {
+ const extension = fileExtension(filename);
+ switch (extension) {
+ case '.html': {
+ return 'html';
+ }
+ case '.json': {
+ return 'json';
+ }
+ case '.ts': {
+ return 'typescript';
+ }
+ default: {
+ return '';
+ }
+ }
+}
+
+export function removeBackticks(text: string): string {
+ return text.replaceAll('`', '');
+}
diff --git a/recipes/src/writer.ts b/recipes/src/writer.ts
new file mode 100644
index 0000000..adcbaea
--- /dev/null
+++ b/recipes/src/writer.ts
@@ -0,0 +1,25 @@
+import * as fs from 'node:fs';
+
+import { Recipe } from './recipe';
+import { Step } from './step';
+
+export abstract class Writer {
+ abstract filename(recipe: Recipe): string;
+
+ write(recipe: Recipe) {
+ const filename = this.filename(recipe);
+ const fd = fs.openSync(filename, 'w');
+
+ this.writePreable(fd, recipe);
+
+ for (let i = 0; i < recipe.steps.length; i++) {
+ this.writeStep(fd, i, recipe.steps[i]);
+ }
+
+ fs.closeSync(fd);
+ }
+
+ protected abstract writeStep(fd: number, index: number, step: Step): void;
+
+ protected abstract writePreable(fd: number, recipe: Recipe): void;
+}
diff --git a/recipes/tsconfig.json b/recipes/tsconfig.json
new file mode 100644
index 0000000..a48af9e
--- /dev/null
+++ b/recipes/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "compilerOptions": {
+ "esModuleInterop": true,
+ "module": "nodenext",
+ "outDir": "./dist",
+ "rootDir": "./src",
+ "target": "ES2022"
+ },
+ "include": ["src"]
+}
diff --git a/typescript/vanilla_rspack/README.md b/typescript/vanilla_rspack/README.md
index 3fa7e2a..d37bc5a 100644
--- a/typescript/vanilla_rspack/README.md
+++ b/typescript/vanilla_rspack/README.md
@@ -1,8 +1,6 @@
-
# Vanilla (no framework) rspack typescript example
-This is almost identical to the vanilla webpack example, as `rspack` is designed to be a drop-in
-replacement for `webpack`.
+This is almost identical to the vanilla webpack example, as `rspack` is designed to be a drop-in replacement for `webpack`.
1. Create initial `package.json` (`npm` project settings)
@@ -16,7 +14,7 @@ replacement for `webpack`.
npm install --save-dev typescript @rspack/core @rspack/cli ts-node ts-loader
```
-3. Create typescript configuration `tsconfig.json` containing:
+3. Create typescript configuration `tsconfig.json` containing
```json
{
@@ -32,7 +30,7 @@ replacement for `webpack`.
}
```
-4. Create webpack configuration `rspack.config.ts` containing:
+4. Create rspack configuration `rspack.config.ts` containing
```typescript
import path from 'path';
@@ -58,7 +56,7 @@ replacement for `webpack`.
export default config;
```
-5. Create HTML file `assets/index.html` containing:
+5. Create HTML file `assets/index.html` containing
```html
@@ -73,18 +71,20 @@ replacement for `webpack`.