From f43b17cabb633db12c42a74a4f6771aca4306e6e Mon Sep 17 00:00:00 2001 From: Mitch Lillie Date: Fri, 7 Nov 2025 10:15:57 -0800 Subject: [PATCH] Add hosted HTML apps prototype to Shopify CLI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit implements a prototype for hosted HTML applications in the Shopify CLI, based on technical requirements for static/hosted apps. ## Key Features - New `hosted_html` extension type specification - Support for pure HTML/CSS/JS apps (no React required) - Iframe sandboxing with srcdoc for security isolation - Hash-based routing support via postMessage API - Static file serving with comprehensive MIME type support - File validation (size limits, HTTPS enforcement) - Dev server integration with sandboxed rendering ## Files Added - `hosted_html.ts`: Extension specification with validation - Middleware: `getHostedHtmlMiddleware()` for sandboxed serving - Test app: Complete sample hosted HTML application - Documentation: HOSTED_HTML_PROTOTYPE.md ## Implementation Details ### Security Model - Iframe with srcdoc creates null origin for isolation - Sandbox attributes: allow-scripts, allow-same-origin, allow-forms - CSP headers restrict frame ancestors - HTTPS enforcement for external resources - 50MB bundle size limit ### Build System - Uses existing `copy_files` build mode - Copies HTML, CSS, JS, images, fonts to dist/ - Validates file types and sizes - Ignores node_modules, .git, test files ### Dev Server - New route handler for hosted_html extensions - Wraps content in sandboxed iframe wrapper - Supports client-side navigation via postMessage - Serves assets from build directory ## Technical Background Based on internal tech docs and discussions about: - Asset management and CDN scalability - Security sandboxing similar to UI extensions - Content-addressable hashing for assets (future work) - Direct API access by default - Escape hatch from UI Extensions framework ## Testing Sample test app included in `test-hosted-app/`: - Demonstrates pure HTML/CSS/JS structure - Client-side routing example - Sandboxed execution validation - Mock API integration ## Future Work - AMF module integration for deployment - Content-addressable asset hashing - Image optimization service integration - Subdomain isolation per app - Session token support for Shopify API 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- HOSTED_HTML_PROTOTYPE.md | 220 ++++++++++++++++++ .../models/extensions/load-specifications.ts | 2 + .../extensions/specifications/hosted_html.ts | 170 ++++++++++++++ .../src/cli/services/dev/extension/server.ts | 4 + .../dev/extension/server/middlewares.ts | 103 ++++++++ .../extensions/my-hosted-html-app/app.js | 83 +++++++ .../extensions/my-hosted-html-app/index.html | 44 ++++ .../my-hosted-html-app/shopify.extension.toml | 12 + .../extensions/my-hosted-html-app/styles.css | 102 ++++++++ test-hosted-app/shopify.app.toml | 15 ++ 10 files changed, 755 insertions(+) create mode 100644 HOSTED_HTML_PROTOTYPE.md create mode 100644 packages/app/src/cli/models/extensions/specifications/hosted_html.ts create mode 100644 test-hosted-app/extensions/my-hosted-html-app/app.js create mode 100644 test-hosted-app/extensions/my-hosted-html-app/index.html create mode 100644 test-hosted-app/extensions/my-hosted-html-app/shopify.extension.toml create mode 100644 test-hosted-app/extensions/my-hosted-html-app/styles.css create mode 100644 test-hosted-app/shopify.app.toml diff --git a/HOSTED_HTML_PROTOTYPE.md b/HOSTED_HTML_PROTOTYPE.md new file mode 100644 index 00000000000..139e5d87edd --- /dev/null +++ b/HOSTED_HTML_PROTOTYPE.md @@ -0,0 +1,220 @@ +# Hosted HTML Apps - CLI Prototype + +This prototype implements support for Hosted HTML applications in the Shopify CLI, based on the technical requirements outlined in the project documentation. + +## Overview + +Hosted HTML apps allow developers to build Shopify apps using standard HTML, CSS, and JavaScript without requiring React or the UI Extensions framework. This provides an escape hatch for use cases that need full control over the HTML structure while maintaining security through iframe sandboxing. + +## Key Features Implemented + +### 1. Extension Specification +- **File**: `packages/app/src/cli/models/extensions/specifications/hosted_html.ts` +- New extension type `hosted_html` with configuration schema +- Support for static file serving (HTML, CSS, JS, images, fonts) +- File size validation (50MB limit) +- Security validation for external resources (HTTPS enforcement) +- Configurable entrypoint (defaults to `index.html`) + +### 2. Build System Integration +- Uses `copy_files` build mode (already supported by CLI) +- Copies all static assets to build directory +- Supports common web file types: + - HTML, CSS, JavaScript, JSON + - Images: PNG, JPG, JPEG, SVG, GIF, WebP + - Fonts: WOFF, WOFF2, TTF, EOT + - Other: ICO + +### 3. Sandboxed Iframe Rendering +- **File**: `packages/app/src/cli/services/dev/extension/server/middlewares.ts` +- Implements security model using iframe with `srcdoc` attribute +- Creates null origin for sandboxed content +- Sandbox attributes: `allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox` +- Hash-based routing support via postMessage API +- CSP headers for additional security + +### 4. Dev Server Support +- **File**: `packages/app/src/cli/services/dev/extension/server.ts` +- New middleware route for hosted HTML extensions +- Automatic content wrapping with sandboxing +- Support for navigation and browser history +- Asset serving from build directory + +## Configuration + +### Extension Configuration (shopify.extension.toml) + +```toml +name = "my-hosted-html-app" +type = "hosted_html" +handle = "my-hosted-html-app" + +# Entry point HTML file (optional, defaults to index.html) +entrypoint = "index.html" + +api_version = "2024-10" + +# Optional: Specify app home extension targets +# [[targeting]] +# target = "admin.app.home" +``` + +### Directory Structure + +``` +my-app/ +├── shopify.app.toml +└── extensions/ + └── my-hosted-html-app/ + ├── shopify.extension.toml + ├── index.html # Entry point + ├── styles.css # Stylesheets + ├── app.js # JavaScript + └── assets/ # Images, fonts, etc. + └── logo.png +``` + +## Security Model + +Based on the technical discussions, this implementation follows these security principles: + +1. **Iframe Sandboxing**: Content is served via iframe with `srcdoc` to create a null origin +2. **HTTPS Enforcement**: External scripts and iframes must use HTTPS +3. **CSP Headers**: Content-Security-Policy headers restrict frame ancestors +4. **Size Limits**: Maximum 50MB bundle size to prevent CDN scaling issues +5. **File Type Validation**: Only whitelisted file types are allowed + +## Routing Support + +The implementation includes hash-based routing support: + +```javascript +// In your app.js +function navigateTo(path) { + // Notify parent frame about navigation + window.parent.postMessage({ + type: 'HOSTED_APP_NAVIGATION', + path: path + }, '*'); +} + +// Listen for navigation updates from parent +window.addEventListener('message', (event) => { + if (event.data.type === 'NAVIGATION_UPDATE') { + // Handle route change + updateUI(event.data.path); + } +}); +``` + +## Test Application + +A sample test application is included in `test-hosted-app/`: + +```bash +cd test-hosted-app +# Install dependencies (if you have a package.json) +# pnpm install + +# Run the dev server +shopify app dev +``` + +The test app demonstrates: +- Pure HTML/CSS/JS structure +- Client-side routing +- Sandboxed iframe execution +- Navigation between pages +- Mock API integration + +## Technical Implementation Details + +### Build Process +1. Extension files are copied to `dist/` directory using `copyFilesForExtension()` +2. Build respects ignore patterns: `node_modules/**`, `.git/**`, test files +3. Output path calculation handles `copy_files` mode with empty `outputFileName` + +### Dev Server Flow +1. Request to `/extensions/:extensionId` is intercepted by `getHostedHtmlMiddleware` +2. Middleware checks if extension type is `hosted_html` +3. Reads entrypoint HTML from build directory +4. Wraps content in sandboxed iframe wrapper HTML +5. Serves with appropriate security headers + +### Validation +- **Pre-deployment**: Validates file types, sizes, and security concerns +- **HTML Validation**: Checks for non-HTTPS external resources +- **Size Validation**: Ensures total bundle stays under 50MB limit +- **Entrypoint Validation**: Confirms entrypoint file exists + +## Future Enhancements + +Based on the technical documents, future work should include: + +1. **Content-Addressable Hashing**: For efficient asset management and deduplication +2. **Image Optimization**: Integration with Imagery service for automatic compression +3. **Subdomain Isolation**: Unique subdomain per app for enhanced security +4. **AMF Module Integration**: Backend work to support deployment to Shopify infrastructure +5. **Direct API Access**: Enable session token support for Shopify API calls +6. **Asset Pipeline**: Separate pipeline for non-code assets with MD5 hashing + +## Known Limitations + +1. **No Backend Module**: This is a CLI-only prototype. Deployment requires AMF module support. +2. **Local Dev Only**: Currently only works with `shopify app dev`, not deployment. +3. **Basic Security**: Security validation is basic. Production needs more comprehensive checks. +4. **No Asset Optimization**: Assets are copied as-is without optimization or compression. +5. **Single HTML File**: Srcdoc approach works best with single-page apps or client-side routing. + +## Testing + +To test the prototype: + +1. Create a new extension with type `hosted_html` +2. Add HTML, CSS, and JS files +3. Configure entrypoint in `shopify.extension.toml` +4. Run `shopify app dev` +5. Access the extension via the dev server + +## References + +- Technical Document: Static/Hosted Apps - Asset Management and Security +- Meeting Notes: Hosted Apps - 2025/11/05 13:12 EST +- Hosted Apps Questions Document + +## Architecture Decisions + +### Why iframe with srcdoc? +- Creates null origin for security isolation +- Prevents direct access to parent frame's location +- Allows controlled communication via postMessage +- Avoids CSP whack-a-mole issues + +### Why copy_files mode? +- Reuses existing CLI infrastructure +- Simple and predictable build process +- No bundling/transpilation overhead +- Preserves developer's original file structure + +### Why hash routing? +- Most resilient within sandboxed iframe context +- Works with null origin restriction +- Doesn't require server-side routing +- Compatible with postMessage navigation API + +## Contributing + +When working on this prototype: + +1. Maintain security boundaries - all external resources must be HTTPS +2. Follow existing CLI patterns for extension specifications +3. Add tests for new validation logic +4. Update this documentation with significant changes +5. Consider CDN scaling impact of any new features + +## Contact + +For questions about this prototype, see: +- Project docs: [Google Drive links from original request] +- Tech lead: Melissa Luu (starting after garden rotation) +- Contributors: Phiroze Noble, Jason Miller, David Cameron diff --git a/packages/app/src/cli/models/extensions/load-specifications.ts b/packages/app/src/cli/models/extensions/load-specifications.ts index 77e37fae7f6..5b8adaa5058 100644 --- a/packages/app/src/cli/models/extensions/load-specifications.ts +++ b/packages/app/src/cli/models/extensions/load-specifications.ts @@ -26,6 +26,7 @@ import uiExtensionSpec from './specifications/ui_extension.js' import webPixelSpec from './specifications/web_pixel_extension.js' import editorExtensionCollectionSpecification from './specifications/editor_extension_collection.js' import channelSpecificationSpec from './specifications/channel.js' +import hostedHtmlSpec from './specifications/hosted_html.js' const SORTED_CONFIGURATION_SPEC_IDENTIFIERS = [ BrandingSpecIdentifier, @@ -66,6 +67,7 @@ function loadSpecifications() { flowTemplateSpec, flowTriggerSpecification, functionSpec, + hostedHtmlSpec, paymentExtensionSpec, posUISpec, productSubscriptionSpec, diff --git a/packages/app/src/cli/models/extensions/specifications/hosted_html.ts b/packages/app/src/cli/models/extensions/specifications/hosted_html.ts new file mode 100644 index 00000000000..ce85f813364 --- /dev/null +++ b/packages/app/src/cli/models/extensions/specifications/hosted_html.ts @@ -0,0 +1,170 @@ +import {createExtensionSpecification, ExtensionFeature} from '../specification.js' +import {BaseSchema} from '../schemas.js' +import {ExtensionInstance} from '../extension-instance.js' +import {loadLocalesConfig} from '../../../utilities/extensions/locales-configuration.js' +import {glob, fileSize, readFile} from '@shopify/cli-kit/node/fs' +import {joinPath, relativePath, extname} from '@shopify/cli-kit/node/path' +import {AbortError} from '@shopify/cli-kit/node/error' +import {outputContent, outputToken} from '@shopify/cli-kit/node/output' +import {zod} from '@shopify/cli-kit/node/schema' + +const kilobytes = 1024 +const megabytes = kilobytes * 1024 + +const BUNDLE_SIZE_LIMIT_MB = 50 +const BUNDLE_SIZE_LIMIT = BUNDLE_SIZE_LIMIT_MB * megabytes + +// Supported file types for hosted HTML apps +const SUPPORTED_FILE_EXTS = [ + '.html', + '.css', + '.js', + '.json', + '.png', + '.jpg', + '.jpeg', + '.svg', + '.gif', + '.webp', + '.woff', + '.woff2', + '.ttf', + '.eot', + '.ico', +] + +export const HostedHtmlSchema = BaseSchema.extend({ + name: zod.string(), + type: zod.literal('hosted_html'), + entrypoint: zod.string().default('index.html'), + // Optional: specify which app home extension points this can target + targeting: zod + .array( + zod.object({ + target: zod.string(), + }), + ) + .optional(), +}) + +export type HostedHtmlSchemaType = zod.infer + +const hostedHtmlSpec = createExtensionSpecification({ + identifier: 'hosted_html', + schema: HostedHtmlSchema, + partnersWebIdentifier: 'hosted_html', + graphQLType: 'hosted_html', + buildConfig: { + mode: 'copy_files', + filePatterns: ['**/*.{html,css,js,json,png,jpg,jpeg,svg,gif,webp,woff,woff2,ttf,eot,ico}'], + ignoredFilePatterns: ['node_modules/**', '.git/**', '**/*.test.*', '**/*.spec.*', '**/test/**', '**/tests/**'], + }, + appModuleFeatures: (_config): ExtensionFeature[] => { + return ['single_js_entry_path', 'bundling', 'app_home_extension'] + }, + preDeployValidation: async (extension) => { + await validateHostedHtmlExtension(extension) + }, + deployConfig: async (config, directory) => { + return { + api_version: config.api_version, + name: config.name, + description: config.description, + entrypoint: config.entrypoint || 'index.html', + localization: await loadLocalesConfig(directory, config.type), + extension_points: config.targeting?.map((target) => ({ + target: target.target, + entrypoint: config.entrypoint || 'index.html', + })), + } + }, + deployableViaService: true, +}) + +async function validateHostedHtmlExtension(extension: ExtensionInstance): Promise { + const config = extension.configuration as HostedHtmlSchemaType + const entrypoint = config.entrypoint || 'index.html' + const entrypointPath = joinPath(extension.directory, entrypoint) + + // Check if entrypoint exists + const allFiles = await glob(joinPath(extension.directory, '**/*'), { + cwd: extension.directory, + ignore: ['node_modules/**', '.git/**', '**/test/**', '**/tests/**'], + }) + + const entrypointExists = allFiles.some((file) => file === entrypointPath) + if (!entrypointExists) { + throw new AbortError( + outputContent`Entrypoint file not found: ${outputToken.path(entrypoint)}`, + 'Make sure your entrypoint file exists in your extension directory.', + ) + } + + // Validate all files + const extensionBytes: number[] = [] + let hasHtmlFile = false + + const validationPromises = allFiles.map(async (filepath) => { + const relativePathName = relativePath(extension.directory, filepath) + const ext = extname(filepath) + + // Check file extension + if (!SUPPORTED_FILE_EXTS.includes(ext)) { + throw new AbortError( + outputContent`Unsupported file type: ${outputToken.path(relativePathName)}`, + `Only these file types are supported: ${SUPPORTED_FILE_EXTS.join(', ')}`, + ) + } + + if (ext === '.html') { + hasHtmlFile = true + // Basic security validation for HTML files + await validateHtmlFile(filepath, relativePathName) + } + + const filesize = await fileSize(filepath) + return filesize + }) + + extensionBytes.push(...(await Promise.all(validationPromises))) + + if (!hasHtmlFile) { + throw new AbortError( + 'Your hosted HTML extension must contain at least one .html file', + 'Add an HTML file to your extension directory.', + ) + } + + // Validate total size + const totalBytes = extensionBytes.reduce((sum, size) => sum + size, 0) + if (totalBytes > BUNDLE_SIZE_LIMIT) { + const humanBundleSize = `${(totalBytes / megabytes).toFixed(2)} MB` + throw new AbortError( + `Your hosted HTML extension exceeds the file size limit (${BUNDLE_SIZE_LIMIT_MB} MB). It's currently ${humanBundleSize}.`, + `Reduce your total file size and try again.`, + ) + } +} + +async function validateHtmlFile(filepath: string, relativePathName: string): Promise { + const content = await readFile(filepath) + + // Basic security checks - prevent common XSS vectors + // External scripts (non-HTTPS) + // Non-HTTPS iframes + const dangerousPatterns = [ + /]*src\s*=\s*["'](?!https:\/\/|\/\/)[^"']*["']/gi, + /]*src\s*=\s*["'](?!https:\/\/|\/\/)[^"']*["']/gi, + ] + + for (const pattern of dangerousPatterns) { + if (pattern.test(content)) { + throw new AbortError( + outputContent`Security concern in ${outputToken.path(relativePathName)}`, + 'External resources must use HTTPS. All scripts and iframes must use secure protocols.', + ) + } + } +} + +export default hostedHtmlSpec diff --git a/packages/app/src/cli/services/dev/extension/server.ts b/packages/app/src/cli/services/dev/extension/server.ts index c9fc81eb43b..95df06dcba2 100644 --- a/packages/app/src/cli/services/dev/extension/server.ts +++ b/packages/app/src/cli/services/dev/extension/server.ts @@ -6,6 +6,7 @@ import { getExtensionPayloadMiddleware, getExtensionPointMiddleware, getExtensionsPayloadMiddleware, + getHostedHtmlMiddleware, getLogMiddleware, noCacheMiddleware, redirectToDevConsoleMiddleware, @@ -30,6 +31,9 @@ export function setupHTTPServer(options: SetupHTTPServerOptions) { httpApp.use(noCacheMiddleware) httpRouter.use('/extensions/dev-console', devConsoleIndexMiddleware) httpRouter.use('/extensions/dev-console/assets/**:assetPath', devConsoleAssetsMiddleware) + // Hosted HTML middleware should be checked before general extension payload + httpRouter.use('/extensions/:extensionId', getHostedHtmlMiddleware(options)) + httpRouter.use('/extensions/:extensionId/', getHostedHtmlMiddleware(options)) httpRouter.use('/extensions/:extensionId', getExtensionPayloadMiddleware(options)) httpRouter.use('/extensions/:extensionId/', getExtensionPayloadMiddleware(options)) httpRouter.use('/extensions/:extensionId/:extensionPointTarget', getExtensionPointMiddleware(options)) diff --git a/packages/app/src/cli/services/dev/extension/server/middlewares.ts b/packages/app/src/cli/services/dev/extension/server/middlewares.ts index a2f179af568..9285b648279 100644 --- a/packages/app/src/cli/services/dev/extension/server/middlewares.ts +++ b/packages/app/src/cli/services/dev/extension/server/middlewares.ts @@ -252,3 +252,106 @@ function getWebsocketUrl(devOptions: GetExtensionsMiddlewareOptions['devOptions' return socket.toString() } + +/** + * Middleware to serve hosted HTML apps with sandboxing via iframe srcdoc + * This implements the security model discussed in the tech docs + */ +export function getHostedHtmlMiddleware({devOptions, getExtensions}: GetExtensionsMiddlewareOptions) { + return async (request: IncomingMessage, response: ServerResponse, next: (err?: Error) => unknown) => { + const extensionID = request.context.params.extensionId + const extension = getExtensions().find((ext) => ext.devUUID === extensionID) + + if (!extension) { + return sendError(response, { + statusCode: 404, + statusMessage: `Extension with id ${extensionID} not found`, + }) + } + + // Only apply this middleware to hosted_html extensions + if (extension.type !== 'hosted_html') { + return next() + } + + const bundlePath = devOptions.appWatcher.buildOutputPath + const extensionOutputPath = extension.getOutputPathForDirectory(bundlePath) + const buildDirectory = extensionOutputPath.replace(extension.outputFileName, '') + + // Get the entrypoint from configuration or default to index.html + const entrypoint = (extension.configuration as {entrypoint?: string}).entrypoint ?? 'index.html' + const entrypointPath = joinPath(buildDirectory, entrypoint) + + const exists = await fileExists(entrypointPath) + if (!exists) { + return sendError(response, { + statusCode: 404, + statusMessage: `Entrypoint file not found: ${entrypoint}`, + }) + } + + // Read the HTML content + const htmlContent = await readFile(entrypointPath) + + // Create a sandboxed iframe wrapper + // This uses srcdoc to ensure the content has a null origin for security + const sandboxedHtml = ` + + + + + + Hosted HTML App - ${extension.configuration.name} + + + + + + + + ` + + response.setHeader('Content-Type', 'text/html') + response.setHeader('X-Frame-Options', 'SAMEORIGIN') + response.setHeader('Content-Security-Policy', "frame-ancestors 'self'") + response.writeHead(200) + response.end(sandboxedHtml) + } +} diff --git a/test-hosted-app/extensions/my-hosted-html-app/app.js b/test-hosted-app/extensions/my-hosted-html-app/app.js new file mode 100644 index 00000000000..28efb0eec5e --- /dev/null +++ b/test-hosted-app/extensions/my-hosted-html-app/app.js @@ -0,0 +1,83 @@ +// Simple client-side routing using hash +let currentRoute = '/'; + +function navigateTo(path) { + currentRoute = path; + updateRoute(); + + // Notify parent frame about navigation (for hash routing support) + if (window.parent !== window) { + window.parent.postMessage({ + type: 'HOSTED_APP_NAVIGATION', + path: path + }, '*'); + } +} + +function updateRoute() { + const routeDisplay = document.getElementById('current-route'); + if (routeDisplay) { + routeDisplay.textContent = currentRoute; + } + + // Simple page content based on route + const content = document.getElementById('content'); + const routeInfo = document.createElement('div'); + + switch(currentRoute) { + case '/page1': + routeInfo.innerHTML = '

Page 1

This is the first test page.

'; + break; + case '/page2': + routeInfo.innerHTML = '

Page 2

This is the second test page.

'; + break; + default: + routeInfo.innerHTML = '

Current route: ' + currentRoute + '

'; + } + + // Keep the route display + content.innerHTML = '

Current route: ' + currentRoute + '

'; + content.appendChild(routeInfo); +} + +// Listen for navigation updates from parent frame +window.addEventListener('message', (event) => { + if (event.data.type === 'NAVIGATION_UPDATE') { + currentRoute = event.data.path || '/'; + updateRoute(); + } +}); + +// Handle browser hash changes +window.addEventListener('hashchange', () => { + currentRoute = window.location.hash.slice(1) || '/'; + updateRoute(); +}); + +// Mock API test function +function testAPI() { + const resultEl = document.getElementById('api-result'); + resultEl.textContent = 'Testing API connection...\n'; + + // Simulate API call + setTimeout(() => { + const mockData = { + status: 'success', + message: 'This would be a real Shopify API call', + timestamp: new Date().toISOString(), + features: [ + 'Direct API enabled by default', + 'Session token support', + 'GraphQL endpoint access' + ] + }; + + resultEl.textContent = JSON.stringify(mockData, null, 2); + }, 500); +} + +// Initialize +updateRoute(); + +console.log('Hosted HTML app loaded successfully!'); +console.log('Running in sandboxed iframe:', window.self !== window.top); diff --git a/test-hosted-app/extensions/my-hosted-html-app/index.html b/test-hosted-app/extensions/my-hosted-html-app/index.html new file mode 100644 index 00000000000..e110f01b2ae --- /dev/null +++ b/test-hosted-app/extensions/my-hosted-html-app/index.html @@ -0,0 +1,44 @@ + + + + + + Hosted HTML App Test + + + +
+

Welcome to Hosted HTML App

+

This is a test of the new hosted HTML extension type for Shopify CLI.

+ +
+

Features Demonstrated:

+
    +
  • Pure HTML/CSS/JS (no React required)
  • +
  • Sandboxed via iframe with srcdoc
  • +
  • Hash-based routing support
  • +
  • Direct file serving from CLI dev server
  • +
+
+ + + +
+

Current route: /

+
+ +
+

API Test:

+ +

+    
+
+ + + + diff --git a/test-hosted-app/extensions/my-hosted-html-app/shopify.extension.toml b/test-hosted-app/extensions/my-hosted-html-app/shopify.extension.toml new file mode 100644 index 00000000000..da2480c0c86 --- /dev/null +++ b/test-hosted-app/extensions/my-hosted-html-app/shopify.extension.toml @@ -0,0 +1,12 @@ +name = "my-hosted-html-app" +type = "hosted_html" +handle = "my-hosted-html-app" + +# Entry point HTML file +entrypoint = "index.html" + +api_version = "2024-10" + +# Optional: Specify app home extension targets +# [[targeting]] +# target = "admin.app.home" diff --git a/test-hosted-app/extensions/my-hosted-html-app/styles.css b/test-hosted-app/extensions/my-hosted-html-app/styles.css new file mode 100644 index 00000000000..a2819e889eb --- /dev/null +++ b/test-hosted-app/extensions/my-hosted-html-app/styles.css @@ -0,0 +1,102 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + line-height: 1.6; + padding: 20px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; +} + +.container { + max-width: 800px; + margin: 0 auto; + background: white; + padding: 30px; + border-radius: 10px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1); +} + +h1 { + color: #333; + margin-bottom: 20px; + font-size: 2.5em; +} + +h2 { + color: #555; + margin-top: 30px; + margin-bottom: 15px; + font-size: 1.5em; +} + +p { + color: #666; + margin-bottom: 15px; +} + +.features { + background: #f8f9fa; + padding: 20px; + border-radius: 8px; + margin: 20px 0; +} + +.features ul { + list-style-position: inside; + color: #444; +} + +.features li { + margin: 8px 0; +} + +.navigation { + margin: 30px 0; +} + +button { + background: #667eea; + color: white; + border: none; + padding: 10px 20px; + border-radius: 5px; + cursor: pointer; + font-size: 1em; + margin: 5px; + transition: background 0.3s ease; +} + +button:hover { + background: #5568d3; +} + +#content { + background: #e9ecef; + padding: 15px; + border-radius: 5px; + margin: 20px 0; +} + +#current-route { + font-weight: bold; + color: #667eea; +} + +.api-test { + margin-top: 30px; +} + +#api-result { + background: #f8f9fa; + padding: 15px; + border-radius: 5px; + margin-top: 10px; + border: 1px solid #dee2e6; + max-height: 300px; + overflow-y: auto; +} diff --git a/test-hosted-app/shopify.app.toml b/test-hosted-app/shopify.app.toml new file mode 100644 index 00000000000..eb8f39e21da --- /dev/null +++ b/test-hosted-app/shopify.app.toml @@ -0,0 +1,15 @@ +# Learn more about configuring your app at https://shopify.dev/docs/apps/tools/cli/configuration + +name = "test-hosted-app" +client_id = "test-client-id" +application_url = "http://localhost:3000" + +[access_scopes] +# Learn more at https://shopify.dev/docs/apps/tools/cli/configuration#access_scopes +scopes = "write_products" + +[build] +automatically_update_urls_on_dev = true + +[webhooks] +api_version = "2024-10"