Skip to content
Closed

Cli #375

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
adff71c
Refactor image handling in content components to use ContentThumbnail
Stivenjs Oct 29, 2025
2fc08a5
Update package.json and pnpm configuration to include theme-studio pa…
Stivenjs Oct 29, 2025
2864e5d
Add preview functionality to StorePage and enhance domain resolution
Stivenjs Oct 30, 2025
29f5550
Update LICENSE to include Shopify Polaris attribution and modify Edit…
Stivenjs Oct 30, 2025
0bf1915
Enhance EditorHeader component with improved tooltip accessibility an…
Stivenjs Oct 30, 2025
27c3352
Enhance ThemeStudio and related components with improved page selecti…
Stivenjs Oct 30, 2025
bf1b0c0
Update hero section and template configurations for improved localiza…
Stivenjs Oct 30, 2025
f7a6dca
Optimize iframe navigation and preview updates in ThemeStudio components
Stivenjs Oct 30, 2025
8829a6a
Enhance Sidebar component with dynamic page title and loading state m…
Stivenjs Oct 30, 2025
06f6f6c
Refactor Sidebar component to integrate SectionGroup for layout sections
Stivenjs Oct 31, 2025
069194f
Refactor SectionGroup and SectionItem components for improved layout …
Stivenjs Oct 31, 2025
4a765cf
Enhance Sidebar component with Divider elements for improved visual s…
Stivenjs Oct 31, 2025
88ca994
Add block configurations to various theme sections for enhanced custo…
Stivenjs Oct 31, 2025
cdb3484
Update hero section to support customizable action buttons and enhanc…
Stivenjs Oct 31, 2025
1c67933
Implement structuredClone polyfill and refactor LiquidEngine to use c…
Stivenjs Nov 2, 2025
a3c948b
Remove unnecessary exports from LiquidEngine class
Stivenjs Nov 2, 2025
e233c4b
Update pnpm version to 10.20.0 across workflows and amplify configura…
Stivenjs Nov 2, 2025
034fdd1
Update Node.js setup in build workflow to use single quotes for consi…
Stivenjs Nov 2, 2025
9eefe64
Enhance CacheInvalidationService with detailed invalidation logic and…
Stivenjs Nov 2, 2025
84ee76e
Enhance Theme Studio with fasttify_attributes filter and sidebar stat…
Stivenjs Nov 3, 2025
de1aa16
Add domain parameter to PreviewPane and useIframeSelection for iframe…
Stivenjs Nov 3, 2025
3f072fc
Enhance ThemeStudio and SettingsPane with image selector component in…
Stivenjs Nov 3, 2025
63cb32a
Refactor iframe selection styles in Theme Studio
Stivenjs Nov 3, 2025
b1e1fba
Enhance ThemeStudio with selectedElementName integration and improved…
Stivenjs Nov 4, 2025
845bd10
Refactor iframe selection styles and improve label positioning in The…
Stivenjs Nov 4, 2025
8c899e2
Refactor hero section and enhance block schema integration in Theme S…
Stivenjs Nov 4, 2025
2ee2d31
Enhance block schema integration and add custom block names in Theme …
Stivenjs Nov 4, 2025
ded5589
Enhance hero section settings with additional headers and info fields
Stivenjs Nov 4, 2025
e169514
Refactor SectionItem component to improve click handling and expand f…
Stivenjs Nov 4, 2025
3f82e83
Enhance sidebar functionality with sub-block selection and improved b…
Stivenjs Nov 4, 2025
aad395b
Add fasttifyAttributesFilter to filters and update related components…
Stivenjs Nov 4, 2025
0438101
Refactor fasttifyAttributesFilter for improved parent block handling …
Stivenjs Nov 4, 2025
64aa8dc
Update pnpm-lock.yaml for AWS SDK version consistency and enhance The…
Stivenjs Nov 4, 2025
a112034
Update session ID generation in DevSessionManagerService to use secur…
Stivenjs Nov 4, 2025
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
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10.18.0
version: 10.20.0

- name: Setup Node.js
uses: actions/setup-node@v4
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/prettier.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10.18.0
version: 10.20.0

- name: Configure Node.js
uses: actions/setup-node@v4
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/unit_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10.18.0
version: 10.20.0

- name: setup node
uses: actions/setup-node@v4
Expand Down
13 changes: 4 additions & 9 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
Portions of this software are derived from Shopify Polaris (https://polaris.shopify.com)
Copyright (c) Shopify Inc.
Licensed under the MIT License.

Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
Expand Down Expand Up @@ -177,15 +181,6 @@

APPENDIX: How to apply the Apache License to your work.

To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.

Copyright 2025 Fasttify LLC

Licensed under the Apache License, Version 2.0 (the "License");
Expand Down
24 changes: 24 additions & 0 deletions NOTICE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Notice

This project uses components and/or design resources from **Shopify Polaris**,
an open-source design system licensed under the **MIT License**.

---

## Copyright and License

**Shopify Polaris**
Copyright (c) Shopify Inc.
Released under the MIT License.

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

---

This project is **not affiliated with, endorsed by, or sponsored by Shopify Inc.**
All trademarks and brand names are the property of their respective owners.
4 changes: 2 additions & 2 deletions amplify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ backend:
commands:
- export NODE_OPTIONS='--max-old-space-size=7000'
- node -e "console.log('Total available heap size (MB):', v8.getHeapStatistics().heap_size_limit / 1024 / 1024)"
- npm install -g pnpm@10.18.0
- npm install -g pnpm@10.20.0
- pnpm install --frozen-lockfile
- npx ampx pipeline-deploy --branch $AWS_BRANCH --app-id $AWS_APP_ID

Expand All @@ -15,7 +15,7 @@ frontend:
commands:
- export NODE_OPTIONS='--max-old-space-size=7000'
- node -e "console.log('Total available heap size (MB):', v8.getHeapStatistics().heap_size_limit / 1024 / 1024)"
- npm install -g pnpm@10.18.0
- npm install -g pnpm@10.20.0
- env | grep -e BUCKET_NAME >> .env.production
- env | grep -e CLOUDFRONT_DOMAIN_NAME >> .env.production
- env | grep -e APP_URL >> .env.production
Expand Down
42 changes: 41 additions & 1 deletion app/[store]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import DevAutoReloadScript from '@/app/[store]/src/components/DevAutoReloadScript';
import { StorePageController } from '@/app/[store]/src/_lib/controllers/store-page-controller';
import { StoreMetadataController } from '@/app/[store]/src/_lib/controllers/store-metadata-controller';
import { PreviewPageController } from '@/app/[store]/src/_lib/controllers/preview-page-controller';
import { isAssetPath } from '@/app/[store]/src/lib/store-page-utils';
import { Metadata } from 'next';

Expand All @@ -11,6 +12,7 @@ interface StorePageProps {
store: string;
}>;
searchParams: Promise<{
domain?: string;
path?: string;
[key: string]: string | string[] | undefined;
}>;
Expand All @@ -19,12 +21,40 @@ interface StorePageProps {
/**
* Página principal de tienda con SSR
* Maneja todas las rutas de tienda: /, /products/slug, /collections/slug, etc
* También maneja /preview?domain=... para previews del Theme Studio
*
* Esta página solo orquesta la llamada al controller, delegando toda la lógica
*/
export default async function StorePage(props: StorePageProps) {
const controller = new StorePageController();
const awaitedParams = await props.params;
const awaitedSearchParams = await props.searchParams;

// Si la ruta es /preview, usar el controlador de preview
if (awaitedParams.store === 'preview' && awaitedSearchParams.domain) {
const previewController = new PreviewPageController();
const domainParam = Array.isArray(awaitedSearchParams.domain)
? awaitedSearchParams.domain[0]
: awaitedSearchParams.domain;
const path = Array.isArray(awaitedSearchParams.path)
? awaitedSearchParams.path[0] || '/'
: awaitedSearchParams.path || '/';

const result = await previewController.handle({
domain: domainParam,
path,
searchParams: awaitedSearchParams,
});

return (
<>
{result.html && <div dangerouslySetInnerHTML={{ __html: result.html }} />}
{process.env.NODE_ENV === 'development' && <DevAutoReloadScript />}
</>
);
}

// Comportamiento normal para rutas de tienda
let html: string | null = null;
let errorHtml: string | null = null;

Expand Down Expand Up @@ -52,7 +82,17 @@ export default async function StorePage(props: StorePageProps) {
* Genera metadata SEO para la página
*/
export async function generateMetadata(props: StorePageProps): Promise<Metadata> {
const { path } = await props.searchParams;
const awaitedParams = await props.params;
const awaitedSearchParams = await props.searchParams;
const { path } = awaitedSearchParams;

// Si es preview, no generar metadata (devolver valores por defecto)
if (awaitedParams.store === 'preview') {
return {
title: 'Preview',
description: 'Theme Studio Preview',
};
}

if (path && isAssetPath(path)) {
return {
Expand Down
38 changes: 38 additions & 0 deletions app/[store]/src/_lib/controllers/preview-page-controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { getCachedRenderResult } from '@/app/[store]/src/lib/store-page-utils';

interface PreviewPageProps {
domain: string;
path: string;
searchParams: Record<string, string | string[] | undefined>;
}

/**
* Controlador para manejar las páginas de preview del Theme Studio
* Renderiza directamente usando el dominio sin pasar por resolveDomainFromParam
*/
export class PreviewPageController {
async handle(props: PreviewPageProps) {
const { domain, path, searchParams } = props;

// Validar que el dominio no sea "preview"
if (!domain || domain === 'preview' || domain.startsWith('preview.')) {
throw new Error('Invalid domain for preview. Domain must be provided.');
}

// Decodificar el dominio si viene codificado
const decodedDomain = decodeURIComponent(domain);

// Validar que el dominio decodificado tenga el formato correcto
if (!decodedDomain.includes('.')) {
throw new Error(`Invalid domain format: ${decodedDomain}`);
}

// Renderizar directamente usando el dominio
const result = await getCachedRenderResult(decodedDomain, path, searchParams);

return {
html: result.html,
metadata: result.metadata,
};
}
}
8 changes: 7 additions & 1 deletion app/[store]/src/_lib/services/domain.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,16 @@ import { DOMAIN_CONFIG, COMMON_ASSETS } from '@/app/[store]/src/_lib/constants/s
export class DomainService {
/**
* Resuelve el dominio de la tienda a partir del parámetro store
* Maneja tanto parámetros codificados como sin codificar
*/
resolveDomainFromParam(store: string): string {
const { BASE_DOMAIN } = DOMAIN_CONFIG;
return store.includes('.') ? store : `${store}.${BASE_DOMAIN}`;

// Decodificar el parámetro si viene codificado (ej: tienda-695a7d7%2Efasttify%2Ecom)
const decodedStore = decodeURIComponent(store);

// Si el store decodificado tiene puntos, ya es un dominio completo
return decodedStore.includes('.') ? decodedStore : `${decodedStore}.${BASE_DOMAIN}`;
}

/**
Expand Down
115 changes: 115 additions & 0 deletions app/api/stores/[storeId]/dev/update/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* Copyright 2025 Fasttify LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { NextRequest, NextResponse } from 'next/server';
import { getNextCorsHeaders } from '@/lib/utils/next-cors';
import { withAuthHandler } from '@/api/_lib/auth-middleware';
import { TemplateLoaderAdapter } from '@/app/api/stores/_lib/dev-server/infrastructure/adapters/template-loader.adapter';
import { SectionRendererAdapter } from '@/app/api/stores/_lib/dev-server/infrastructure/adapters/section-renderer.adapter';
import { UpdateSectionSettingUseCase } from '@/app/api/stores/_lib/dev-server/application/use-cases/update-section-setting.use-case';
import { UpdateBlockSettingUseCase } from '@/app/api/stores/_lib/dev-server/application/use-cases/update-block-setting.use-case';
import { UpdateSubBlockSettingUseCase } from '@/app/api/stores/_lib/dev-server/application/use-cases/update-sub-block-setting.use-case';
import { devSessionManager } from '@/app/api/stores/_lib/dev-server/infrastructure/services/dev-session-manager.service';
import {
broadcastChangeApplied,
broadcastRenderError,
} from '@/app/api/stores/_lib/dev-server/controllers/sse-controller';

const templateLoader = new TemplateLoaderAdapter();
const sectionRenderer = new SectionRendererAdapter();
const updateSectionSettingUseCase = new UpdateSectionSettingUseCase(templateLoader, sectionRenderer);
const updateBlockSettingUseCase = new UpdateBlockSettingUseCase(templateLoader, sectionRenderer);
const updateSubBlockSettingUseCase = new UpdateSubBlockSettingUseCase(templateLoader, sectionRenderer);

/**
* Endpoint POST para recibir actualizaciones de settings desde el cliente
* Procesa el cambio y envía la actualización por SSE
*/
export const POST = withAuthHandler(
async (request: NextRequest, { storeId, corsHeaders }) => {
try {
const body = await request.json();
const { type, payload, templateType } = body;

if (!templateType) {
return NextResponse.json({ error: 'Missing templateType' }, { status: 400, headers: corsHeaders });
}

// Obtener o crear sesión
let session = devSessionManager.getSession(storeId, templateType);
if (!session) {
// Cargar template inicial y crear sesión
const template = await templateLoader.loadTemplate(storeId, templateType);
session = devSessionManager.createOrGetSession(storeId, templateType, template);
}

let result;

switch (type) {
case 'UPDATE_SECTION_SETTING': {
result = await updateSectionSettingUseCase.execute(payload, templateType, session.template);
if (result.updatedTemplate) {
devSessionManager.updateSessionTemplate(storeId, templateType, result.updatedTemplate);
}
break;
}

case 'UPDATE_BLOCK_SETTING': {
result = await updateBlockSettingUseCase.execute(payload, templateType, session.template);
if (result.updatedTemplate) {
devSessionManager.updateSessionTemplate(storeId, templateType, result.updatedTemplate);
}
break;
}

case 'UPDATE_SUB_BLOCK_SETTING': {
result = await updateSubBlockSettingUseCase.execute(payload, templateType, session.template);
if (result.updatedTemplate) {
devSessionManager.updateSessionTemplate(storeId, templateType, result.updatedTemplate);
}
break;
}

default:
return NextResponse.json({ error: 'Invalid message type' }, { status: 400, headers: corsHeaders });
}

// Enviar resultado a través de SSE
if (result.success) {
broadcastChangeApplied(storeId, templateType, result);
} else {
broadcastRenderError(storeId, templateType, result.error || 'Unknown error', result.sectionId);
}

return NextResponse.json({ success: true }, { headers: corsHeaders });
} catch (error) {
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Unknown error' },
{ status: 500, headers: corsHeaders }
);
}
},
{
requireStoreOwnership: true,
storeIdSource: 'params',
storeIdParamName: 'storeId',
}
);

export async function OPTIONS(request: NextRequest) {
const corsHeaders = await getNextCorsHeaders(request);
return new Response(null, { status: 204, headers: corsHeaders });
}
46 changes: 46 additions & 0 deletions app/api/stores/[storeId]/dev/ws/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright 2025 Fasttify LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { NextRequest } from 'next/server';
import { withAuthHandler } from '@/api/_lib/auth-middleware';
import { handleSSEConnection } from '@/app/api/stores/_lib/dev-server/controllers/sse-controller';

/**
* Endpoint SSE (Server-Sent Events) para hot-reload del editor
* Similar a template-dev pero específico para el Theme Studio
*/
export const GET = withAuthHandler(
async (request: NextRequest, { storeId }) => {
const url = new URL(request.url);
if (!url.searchParams.has('storeId')) {
url.searchParams.set('storeId', storeId);
}

const modifiedRequest = new NextRequest(url, {
method: request.method,
headers: request.headers,
body: request.body,
signal: request.signal,
});

return handleSSEConnection(modifiedRequest);
},
{
requireStoreOwnership: true,
storeIdSource: 'params',
storeIdParamName: 'storeId',
}
);
Loading