Skip to content

POC: codegen from plugin.json #1563

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

Draft
wants to merge 7 commits into
base: main
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

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { Compiler } from 'webpack';
import { watchPluginJson, stopWatchingPluginJson } from './watchPluginJson';

/**
* Webpack plugin that watches plugin.json and regenerates TypeScript code
* during development.
*/
export class CodeGenPlugin {
private watching = false;

apply(compiler: Compiler) {
// Start watching when webpack enters watch mode
compiler.hooks.watchRun.tap('CodeGenPlugin', () => {
if (this.watching) {
return;
}

this.watching = true;
watchPluginJson();
});

// Stop watching when webpack exits
compiler.hooks.shutdown.tap('CodeGenPlugin', () => {
this.watching = false;
stopWatchingPluginJson();
});

// Generate code on initial build
compiler.hooks.beforeRun.tapAsync('CodeGenPlugin', async (_, callback) => {
console.log('=========== compiler.hooks.beforeRun ============');
try {
watchPluginJson();
callback();
} catch (error) {
callback(error as Error);
}
});

// Prettify after file changes in watch mode
compiler.hooks.afterCompile.tapAsync('CodeGenPlugin', async (_, callback) => {
callback();
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { CodeGenerator } from '../types';
import { PluginSchema } from '../../../types/pluginSchema';
import { FILE_HEADER_COMMENT } from '../utils';
import { CONFIG_DIR } from '../../constants';

function canGenerate(pluginJson: PluginSchema) {
return Boolean(pluginJson?.includes?.length);
}

function generate(pluginJson: PluginSchema) {
if (!pluginJson?.includes?.length) {
return '';
}

// Create dynamic import statement
const imports = 'Include';

// Generate includes enum if includes exist
const includesEnum = `export enum PluginIncludePaths {
${pluginJson.includes
.map((inc) => `${inc.name?.replace(/\s+/g, '')} = "${inc.path?.replace(/%PLUGIN_ID%/, pluginJson.id)}"`)
.join(',\n')}
}`;

// Generate includes map if includes exist
const includesMap = `export const PluginIncludes: ReadonlyMap<PluginIncludePaths, Include> = new Map([
${pluginJson.includes
.map((inc) => {
const enumKey = inc.name?.replace(/\s+/g, '');
// Create the include object without JSON.stringify to preserve the enum reference
const include = {
...inc,
path: `PluginIncludePaths.${enumKey}`,
};
// Convert to string but replace the quoted enum reference with the actual reference
const includeStr = JSON.stringify(include).replace(
`"PluginIncludePaths.${enumKey}"`,
`PluginIncludePaths.${enumKey}`
);
return `[PluginIncludePaths.${enumKey}, ${includeStr}]`;
})
.join(',\n')}
]);`;

const fileContent = `${FILE_HEADER_COMMENT}
import { ${imports} } from '../../${CONFIG_DIR}/types/pluginSchema';

${includesEnum}

${includesMap}
`;

return fileContent;
}

export const pluginIncludes: CodeGenerator = {
name: 'pluginIncludes',
fileName: 'pluginIncludes.ts',
canGenerate,
generate,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import { FileWriter, FileReader, Prettifier } from './types';
import { CodeGenerator } from './types';
import { getPluginJsonPath } from '../utils';
import { getCodegenDirPath, logError, logInfo, logWarning, prettifyFile } from './utils';
import { CODEGEN_DIR, SOURCE_DIR } from '../constants';
import { PluginSchema } from '../../types/pluginSchema';
import { pluginIncludes } from './generators/pluginIncludes';

const codeGenerators: CodeGenerator[] = [pluginIncludes];

const fileWriter: FileWriter = (file, content) => {
const outputFile = path.join(process.cwd(), SOURCE_DIR, CODEGEN_DIR, file);
writeFileSync(outputFile, content);
};

const fileReader: FileReader = (file) => {
return readFileSync(file, 'utf-8');
};

const prettifier: Prettifier = prettifyFile;

export function generateCode() {
const pluginJsonPath = getPluginJsonPath();
const pluginJson: PluginSchema = JSON.parse(fileReader(pluginJsonPath));
const generators = codeGenerators.filter((generator) => generator.canGenerate(pluginJson));
if (!generators.length) {
logWarning('No code generators found for plugin.json');
return;
}

// Create codegen directory if it doesn't exist
if (!existsSync(getCodegenDirPath())) {
mkdirSync(getCodegenDirPath());
}

generators.forEach((generator) => {
try {
logInfo(`Generating code for`, generator.name);
const code = generator.generate(pluginJson);
fileWriter(generator.fileName, code);
prettifier(generator.fileName);
logInfo(`Code generated and prettified for`, generator.name);
} catch (error) {
logError(`Error generating code for ${generator.name}: ${error}`);
}
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { PluginSchema } from '../../types/pluginSchema';

export interface CodeGenerator {
name: string;
fileName: string;
canGenerate: (pluginJson: PluginSchema) => boolean;
generate: (pluginJson: PluginSchema) => string;
}

export type FileWriter = (file: string, content: string) => void;
export type FileReader = (file: string) => string;
export type Prettifier = (file: string) => void;
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import fs from 'fs';
import { execFile as nodeExecFile } from 'node:child_process';
import path from 'node:path';
import { promisify } from 'node:util';
import { CODEGEN_DIR, CONFIG_DIR, SOURCE_DIR } from '../constants';

const execFile = promisify(nodeExecFile);

export async function prettifyFile(fileName: string) {
try {
const filePath = path.join(process.cwd(), SOURCE_DIR, CODEGEN_DIR, fileName);
if (!fs.existsSync(filePath)) {
return;
}

const command = 'npx';
const args = ['prettier', '--write', filePath];
await execFile(command, args);
} catch (error) {
console.error('Failed to prettify generated file:', error);
}
}

export const FILE_HEADER_COMMENT = `// DO NOT EDIT THIS FILE. IT IS AUTOMATICALLY GENERATED FROM PLUGIN.JSON`;

export const colors = {
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m',
reset: '\x1b[0m',
};

export function logInfo(message: string, args: string) {
console.log(`codegen --> ${message}${colors.green} ${args}${colors.reset}`);
console.log('');
}

export function logWarning(message: string) {
console.log(`codegen --> ${colors.yellow}${message}${colors.reset}`);
console.log('');
}

export function logError(message: string) {
console.log(`codegen --> ${colors.red}${message}${colors.reset}`);
console.log('');
}

export function getCodegenDirPath() {
return path.join(process.cwd(), SOURCE_DIR, CODEGEN_DIR);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { watch, FSWatcher } from 'node:fs';
import { getPluginJsonPath } from '../utils';
import { generateCode } from './main';
import { colors } from './utils';

let watcher: FSWatcher | null = null;
const start = `${colors.green}============================================ <CodeGenPlugin> ============================================${colors.reset}`;
const end = `${colors.green}============================================ </CodeGenPlugin> ============================================${colors.reset}`;
/**
* Watches the plugin.json file and regenerates TypeScript types when it changes
*/
export function watchPluginJson() {
const pluginJsonPath = getPluginJsonPath();

// Clean up existing watcher if any
stopWatchingPluginJson();

// Generate initial values
try {
console.log(start);
console.log('');
generateCode();
console.log(end);
console.log('');
} catch (error) {
console.error('Failed to generate plugin types:', error);
}

// Start watching for changes
watcher = watch(pluginJsonPath, async (eventType) => {
if (eventType === 'change') {
console.log(start);
console.log('');
generateCode();
console.log(end);
console.log('');
}
});

return watcher;
}

/**
* Stop watching plugin.json file
*/
export function stopWatchingPluginJson() {
if (watcher) {
watcher.close();
watcher = null;
}
}
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export const SOURCE_DIR = 'src';
export const DIST_DIR = 'dist';
export const CODEGEN_DIR = 'codegen';
export const CONFIG_DIR = '.config';
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,12 @@ export function getPackageJson() {
return require(path.resolve(process.cwd(), 'package.json'));
}

export function getPluginJsonPath() {
return path.resolve(process.cwd(), `${SOURCE_DIR}/plugin.json`);
}

export function getPluginJson() {
return require(path.resolve(process.cwd(), `${SOURCE_DIR}/plugin.json`));
return require(getPluginJsonPath());
}

export function getCPConfigVersion() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import VirtualModulesPlugin from 'webpack-virtual-modules';
import { BuildModeWebpackPlugin } from './BuildModeWebpackPlugin';
import { DIST_DIR, SOURCE_DIR } from './constants';
import { getCPConfigVersion, getEntries, getPackageJson, getPluginJson, hasReadme, isWSL } from './utils';

import { CodeGenPlugin } from './codegen/CodeGenPlugin';
const pluginJson = getPluginJson();
const cpVersion = getCPConfigVersion();

Expand Down Expand Up @@ -194,6 +194,7 @@ const config = async (env: Env): Promise<Configuration> => {

plugins: [
new BuildModeWebpackPlugin(),
new CodeGenPlugin(),
virtualPublicPath,
// Insert create plugin version information into the bundle
new BannerPlugin({
Expand Down
3 changes: 3 additions & 0 deletions packages/create-plugin/templates/common/gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,6 @@ ci/
.idea

.eslintcache

# Code generated by create-plugin
/codegen
Loading