Skip to content
Merged
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
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,17 @@ Add your public URL + /mcp to ChatGPT's `"Settings > Connectors"` .
fastapps create additional-widget
```

### Using Widget Templates

FastApps provides pre-built templates to jumpstart your widget development:

```bash
# Create widget from a template
fastapps create my-list --list # Vertical list with items
fastapps create my-carousel --carousel # Horizontal scrolling cards
fastapps create my-albums --albums # Photo gallery viewer
```


### Editing Your Widget

Expand Down
9 changes: 0 additions & 9 deletions fastapps/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,4 @@ async def execute(self, input_data) -> Dict[str, Any]:
"WidgetBuildResult",
"Field",
"ConfigDict",
# Dev server API
"start_dev_server",
"start_dev_server_with_config",
"get_server_info",
"run_dev_server",
"DevServerConfig",
"ServerInfo",
"DevServerError",
"ProjectNotFoundError",
] + _auth_exports
129 changes: 99 additions & 30 deletions fastapps/builder/build-all.mts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ import crypto from "crypto";
const pkgPath = path.join(process.cwd(), "package.json");
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));

// Auto-detect and import Tailwind CSS if available
let tailwindcss: any = null;
try {
const tailwindModule = await import("@tailwindcss/vite");
tailwindcss = tailwindModule.default;
console.log("✓ Tailwind CSS detected");
} catch (e) {
// Tailwind not installed, skip
}

// Find all widget directories with index.{tsx,jsx}
const widgetDirs = fg.sync("widgets/*/", { onlyDirectories: true });
const entries = widgetDirs.map((dir) => {
Expand All @@ -18,10 +28,20 @@ const entries = widgetDirs.map((dir) => {
}).filter(Boolean);
const outDir = "assets";

// Determine build mode: 'hosted' (default) or 'inline'
const modeEnv = (process.env.MODE ?? "").toLowerCase();
const MODE: "hosted" | "inline" = modeEnv === "inline" ? "inline" : "hosted";

// Global CSS that applies to all widgets
const GLOBAL_CSS_PATH = path.resolve("widgets/index.css");
const PER_ENTRY_CSS_GLOB = "**/*.{css,pcss,scss,sass}";
const PER_ENTRY_CSS_IGNORE = ["**/*.module.*"];

function wrapEntryPlugin(
virtualId: string,
entryFile: string,
widgetName: string
widgetName: string,
cssPaths: string[]
): Plugin {
return {
name: `virtual-entry-wrapper:${entryFile}`,
Expand All @@ -33,8 +53,14 @@ function wrapEntryPlugin(
return null;
}

// Import CSS files (global first, then per-entry)
const cssImports = cssPaths
.map((css) => `import ${JSON.stringify(css)};`)
.join("\n");

// Automatically add mounting logic - no _app.jsx needed!
return `
${cssImports}
import React from 'react';
import { createRoot } from 'react-dom/client';
import Component from ${JSON.stringify(entryFile)};
Expand All @@ -61,13 +87,32 @@ for (const file of entries) {
const name = path.basename(path.dirname(file));

const entryAbs = path.resolve(file);
const entryDir = path.dirname(entryAbs);

// Collect CSS paths: global first, then per-entry
const cssPaths: string[] = [];

// Add global CSS if it exists
if (fs.existsSync(GLOBAL_CSS_PATH)) {
cssPaths.push(GLOBAL_CSS_PATH);
}

// Add per-entry CSS
const perEntryCss = fg.sync(PER_ENTRY_CSS_GLOB, {
cwd: entryDir,
absolute: true,
dot: false,
ignore: PER_ENTRY_CSS_IGNORE,
});
cssPaths.push(...perEntryCss);

const virtualId = `\0virtual-entry:${entryAbs}`;

const createConfig = (): InlineConfig => ({
plugins: [
wrapEntryPlugin(virtualId, entryAbs, name),
wrapEntryPlugin(virtualId, entryAbs, name, cssPaths),
react(),
...(tailwindcss ? [tailwindcss()] : []),
{
name: "remove-manual-chunks",
outputOptions(options) {
Expand Down Expand Up @@ -143,32 +188,56 @@ console.groupEnd();

console.log("new hash: ", h);

for (const name of builtNames) {
const dir = outDir;
const htmlPath = path.join(dir, `${name}-${h}.html`);
const cssPath = path.join(dir, `${name}-${h}.css`);
const jsPath = path.join(dir, `${name}-${h}.js`);

const css = fs.existsSync(cssPath)
? fs.readFileSync(cssPath, { encoding: "utf8" })
: "";
const js = fs.existsSync(jsPath)
? fs.readFileSync(jsPath, { encoding: "utf8" })
: "";

const cssBlock = css ? `\n <style>\n${css}\n </style>\n` : "";
const jsBlock = js ? `\n <script type="module">\n${js}\n </script>` : "";

const html = [
"<!doctype html>",
"<html>",
`<head>${cssBlock}</head>`,
"<body>",
` <div id="${name}-root"></div>${jsBlock}`,
"</body>",
"</html>",
].join("\n");
fs.writeFileSync(htmlPath, html, { encoding: "utf8" });
console.log(`${htmlPath} (generated)`);
if (MODE === "inline") {
for (const name of builtNames) {
const dir = outDir;
const htmlPath = path.join(dir, `${name}-${h}.html`);
const cssPath = path.join(dir, `${name}-${h}.css`);
const jsPath = path.join(dir, `${name}-${h}.js`);

const css = fs.existsSync(cssPath)
? fs.readFileSync(cssPath, { encoding: "utf8" })
: "";
const js = fs.existsSync(jsPath)
? fs.readFileSync(jsPath, { encoding: "utf8" })
: "";

const cssBlock = css ? `\n <style>\n${css}\n </style>\n` : "";
const jsBlock = js ? `\n <script type="module">\n${js}\n </script>` : "";

const html = [
"<!doctype html>",
"<html>",
`<head>${cssBlock}</head>`,
"<body>",
` <div id="${name}-root"></div>${jsBlock}`,
"</body>",
"</html>",
].join("\n");
fs.writeFileSync(htmlPath, html, { encoding: "utf8" });
console.log(`${htmlPath} (generated inline)`);
}
} else {
const defaultBaseUrl = "http://localhost:4444";
const baseUrlCandidate = process.env.BASE_URL?.trim() ?? "";
const baseUrlRaw = baseUrlCandidate || defaultBaseUrl;
const normalizedBaseUrl = baseUrlRaw.replace(/\/+$/, "");
console.log(`Using BASE_URL: ${normalizedBaseUrl}`);
for (const name of builtNames) {
const dir = outDir;
const htmlPath = path.join(dir, `${name}-${h}.html`);
const html = `<!doctype html>
<html>
<head>
<script type="module" src="${normalizedBaseUrl}/${name}-${h}.js"></script>
<link rel="stylesheet" href="${normalizedBaseUrl}/${name}-${h}.css">
</head>
<body>
<div id="${name}-root"></div>
</body>
</html>
`;
fs.writeFileSync(htmlPath, html, { encoding: "utf8" });
console.log(`${htmlPath} (generated with external references)`);
}
}

56 changes: 40 additions & 16 deletions fastapps/builder/compiler.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
import platform
import re
import shutil
Expand Down Expand Up @@ -31,23 +32,39 @@ def __init__(self, project_root: Path | str):
self.widgets_dir = self.project_root / "widgets"
self.framework_dir = Path(__file__).parent

def build_all(self) -> Dict[str, WidgetBuildResult]:
def build_all(self, mode: str = "hosted") -> Dict[str, WidgetBuildResult]:
"""
Build all widgets in the project.

Args:
mode: Build mode - "hosted" (default, external JS/CSS references) or
"inline" (self-contained HTML)

Returns:
Dictionary mapping widget names to build results.
"""
# 1. Auto-discover widgets
self._discover_widgets()

# 2. Copy framework's build script to project (if not exists)
# 2. Ensure unified build script exists in project (if not exists)
self._ensure_build_script()

# 3. Run build (Windows-compatible)
npx_cmd = "npx.cmd" if platform.system() == "Windows" else "npx"
build_script = "build-all.mts"

# Pass mode and BASE_URL (for hosted) via environment
env = os.environ.copy()
# Explicit mode for the script
env["MODE"] = mode
if mode == "hosted":
env["BASE_URL"] = env.get("BASE_URL", "http://localhost:4444")

subprocess.run(
[npx_cmd, "tsx", "build-all.mts"], cwd=self.project_root, check=True
[npx_cmd, "tsx", build_script],
cwd=self.project_root,
check=True,
env=env
)

# 4. Parse results
Expand All @@ -57,25 +74,32 @@ def _ensure_build_script(self):
"""
Ensure build script exists.

The build script is provided by flick-react NPM package.
Users should have 'flick-react' installed as a devDependency.
Args:
mode: Build mode - determines which script to copy
"""
project_build_script = self.project_root / "build-all.mts"
# Use unified build script name
script_name = "build-all.mts"

# Check if flick-react is installed in node_modules
chatjs_build_script = (
self.project_root / "node_modules" / "fastapps" / "build-all.mts"
)
project_build_script = self.project_root / script_name
framework_build_script = self.framework_dir / script_name

# Copy from framework if not exists
if not project_build_script.exists():
if chatjs_build_script.exists():
# Copy from node_modules
shutil.copy(chatjs_build_script, project_build_script)
print("Copied build script from fastapps package")
if framework_build_script.exists():
shutil.copy(framework_build_script, project_build_script)
print(f"Copied {script_name} from FastApps framework")
else:
raise FileNotFoundError(
"build-all.mts not found. Please install fastapps: npm install --save-dev fastapps"
# Fallback: check node_modules
node_modules_script = (
self.project_root / "node_modules" / "fastapps" / script_name
)
if node_modules_script.exists():
shutil.copy(node_modules_script, project_build_script)
print(f"Copied {script_name} from fastapps package")
else:
raise FileNotFoundError(
f"{script_name} not found. Please install fastapps: npm install --save-dev fastapps"
)

def _discover_widgets(self):
"""
Expand Down
Loading
Loading