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
203 changes: 174 additions & 29 deletions packages/vite-plugin/scripts/build-templates.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,64 +8,202 @@ const __dirname = dirname(__filename);
const projectRoot = join(__dirname, '..');

function buildTemplatesWithTsc() {
console.log('Compiling templates with TypeScript...');
console.log('Building templates with bundling...');

// Build service worker with bundled dependencies
buildBundledServiceWorker();

// Build register template (no bundling needed)
buildRegisterTemplate();
}

function buildBundledServiceWorker() {
console.log('Building bundled service worker...');

const outputPath = join(
projectRoot,
'src/generated/service-worker-template.ts',
);

// Create a single bundled TypeScript file first
const bundledTsPath = join(
projectRoot,
'src/templates/bundled-service-worker.ts',
);
createBundledTsFile(bundledTsPath);

// Clean previous compilation
execSync(`rm -rf ${join(projectRoot, 'src/generated-js')}`, {
cwd: projectRoot,
});

// Compile all templates with unified config
// Compile the bundled file with TypeScript
try {
console.log('Compiling templates...');
execSync(`npx tsc --project tsconfig.templates.json`, {
execSync(`mkdir -p ${join(projectRoot, 'src/generated-js')}`, {
cwd: projectRoot,
stdio: 'inherit',
});

execSync(
`npx tsc ${bundledTsPath} --target ES2022 --module ESNext --outDir src/generated-js --lib ES2022,WebWorker,DOM --skipLibCheck`,
{
cwd: projectRoot,
stdio: 'inherit',
},
);
} catch (error) {
console.error('Template TypeScript compilation failed:', error.message);
console.error('TypeScript compilation failed:', error.message);
process.exit(1);
}

// Read compiled JavaScript files and create template exports
processCompiledServiceWorker();
processCompiledRegister();
// Read compiled JavaScript file
const compiledPath = join(
projectRoot,
'src/generated-js/bundled-service-worker.js',
);
const jsContent = readFileSync(compiledPath, 'utf-8');

// Clean up generated JS files
// Create the template export
const templateContent = `// Auto-generated template - do not edit manually
export const SW_TEMPLATE = ${JSON.stringify(jsContent)};
`;

writeFileSync(outputPath, templateContent);

// Clean up temporary files
execSync(`rm -f ${bundledTsPath}`, { cwd: projectRoot });
execSync(`rm -rf ${join(projectRoot, 'src/generated-js')}`, {
cwd: projectRoot,
});

console.log(`✓ Generated: ${outputPath}`);
}

function processCompiledServiceWorker() {
const compiledPath = join(projectRoot, 'src/generated-js/service-worker.js');
const outputPath = join(
projectRoot,
'src/generated/service-worker-template.ts',
function createBundledTsFile(outputPath) {
// Read all source files
const databaseContent = readFileSync(
join(projectRoot, 'src/templates/database.ts'),
'utf-8',
);
const copilotContent = readFileSync(
join(projectRoot, 'src/templates/workerify-sw-copilot.ts'),
'utf-8',
);

// Process the files to remove imports/exports and make them compatible
const processedDatabase = databaseContent.replace(/^export\s+/gm, '');

const processedCopilot = copilotContent
.replace(/^import\s+.*?from\s+.*?;?\s*$/gm, '')
.replace(/^export\s+/gm, '');

// Create the bundled TypeScript file
const bundledContent = `// === Bundled Workerify Service Worker ===
// This file is auto-generated - do not edit manually

${processedDatabase}

${processedCopilot}

// Main service worker
console.log('[Workerify SW] Service worker script loaded');
const { onClientsClaim, onFetch } = init(self as any);

self.addEventListener('install', () => {
console.log('[Workerify SW] Installing');
self.skipWaiting();
});

self.addEventListener('activate', (e: ExtendableEvent) => {
console.log('[Workerify SW] Activating');
console.log(
`Processing compiled service worker: ${compiledPath} -> ${outputPath}`,
'[Workerify SW] Will start intercepting requests for scope:',
self.registration.scope,
);
e.waitUntil(
self.clients.claim().then(() => {
onClientsClaim();
}),
);
});

const jsContent = readFileSync(compiledPath, 'utf-8');

// Create the template export
const templateContent = `// Auto-generated template - do not edit manually
export const SW_TEMPLATE = ${JSON.stringify(jsContent)};
`;
// Test if fetch listener is working at all
console.log('[Workerify SW] Adding fetch event listener...');
self.addEventListener('fetch', (event: FetchEvent) => {
event.respondWith(
(async () => {
const response = await onFetch(event);
if (response) {
return response;
}
// @ts-expect-error
return fetch(event);
})(),
);
});`;

writeFileSync(outputPath, templateContent);
console.log(`✓ Generated: ${outputPath}`);
writeFileSync(outputPath, bundledContent);
}

function processCompiledRegister() {
const compiledPath = join(projectRoot, 'src/generated-js/register.js');
function buildRegisterTemplate() {
console.log('Building register template...');

const outputPath = join(projectRoot, 'src/generated/register-template.ts');
const registerContent = readFileSync(
join(projectRoot, 'src/templates/register.ts'),
'utf-8',
);

console.log(`Processing compiled register: ${compiledPath} -> ${outputPath}`);
// Create a single bundled TypeScript file first
const bundledTsPath = join(projectRoot, 'src/templates/bundled-register.ts');

const jsContent = readFileSync(compiledPath, 'utf-8');
// Remove imports but keep exports since they need to be preserved
const processedRegister = registerContent.replace(
/^import\s+.*?from\s+.*?;?\s*$/gm,
'',
);

const bundledContent = `// === Bundled Register Template ===
// This file is auto-generated - do not edit manually

${processedRegister}`;

writeFileSync(bundledTsPath, bundledContent);

// Clean previous compilation
execSync(`rm -rf ${join(projectRoot, 'src/generated-js')}`, {
cwd: projectRoot,
});

// Compile the bundled file with TypeScript
try {
execSync(`mkdir -p ${join(projectRoot, 'src/generated-js')}`, {
cwd: projectRoot,
});

execSync(
`npx tsc ${bundledTsPath} --target ES2022 --module ESNext --outDir src/generated-js --lib ES2022,DOM --skipLibCheck`,
{
cwd: projectRoot,
stdio: 'inherit',
},
);
} catch (error) {
console.error('TypeScript compilation failed:', error.message);
process.exit(1);
}

// Read compiled JavaScript file
const compiledPath = join(
projectRoot,
'src/generated-js/bundled-register.js',
);
let jsContent = readFileSync(compiledPath, 'utf-8');

// Remove leading comments that cause Vite parsing issues
jsContent = jsContent
.replace(/^\/\/.*$/gm, '') // Remove single-line comments
.replace(/^\s*$/gm, '') // Remove empty lines
.replace(/^[\s\n]+/, ''); // Remove leading whitespace/newlines

// Create a function that generates the register module with placeholder replacement
const templateContent = `// Auto-generated template - do not edit manually
Expand All @@ -80,6 +218,13 @@ export function getRegisterModule(publicSwUrl: string, scope: string, swFileName
`;

writeFileSync(outputPath, templateContent);

// Clean up temporary files
execSync(`rm -f ${bundledTsPath}`, { cwd: projectRoot });
execSync(`rm -rf ${join(projectRoot, 'src/generated-js')}`, {
cwd: projectRoot,
});

console.log(`✓ Generated: ${outputPath}`);
}

Expand All @@ -88,7 +233,7 @@ execSync(`mkdir -p ${join(projectRoot, 'src/generated')}`, {
cwd: projectRoot,
});

// Build templates using TypeScript compiler
// Build templates using bundling approach
buildTemplatesWithTsc();

console.log('✓ All templates built successfully with tsc');
9 changes: 9 additions & 0 deletions packages/vite-plugin/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ export interface WorkerifyPluginOptions {
swFileName?: string;
}

export type {
ExtendableEvent,
FetchEvent,
Route,
ServiceWorkerGlobalScope,
} from './service-worker';
// Re-export service worker functions for manual usage
export { init } from './service-worker';

export default function workerifyPlugin(
opts: WorkerifyPluginOptions = {},
): Plugin {
Expand Down
20 changes: 20 additions & 0 deletions packages/vite-plugin/src/service-worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export type { Route } from './templates/database';
export { init } from './templates/workerify-sw-copilot';

// Re-export commonly used types for service worker development
export interface ServiceWorkerGlobalScope extends EventTarget {
registration: ServiceWorkerRegistration;
clients: Clients;
skipWaiting(): Promise<void>;
}

export interface FetchEvent extends ExtendableEvent {
request: Request;
clientId: string;
resultingClientId?: string;
respondWith(response: Promise<Response> | Response): void;
}

export interface ExtendableEvent extends Event {
waitUntil(promise: Promise<void>): void;
}
100 changes: 100 additions & 0 deletions packages/vite-plugin/src/templates/database.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
export type Route = { method?: string; path: string; match?: string };

// IndexedDB configuration
const DB_NAME = 'workerify-sw-state';
const DB_VERSION = 1;
const STORE_NAME = 'state';

// Initialize IndexedDB
async function initDB(): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);

request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);

request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
if (!db.objectStoreNames.contains(STORE_NAME)) {
db.createObjectStore(STORE_NAME);
}
};
});
}

// Save state to IndexedDB
export async function saveState({
clientConsumerMap,
consumerRoutesMap,
}: {
clientConsumerMap: Map<string, string>;
consumerRoutesMap: Map<string, Array<Route>>;
}) {
try {
const db = await initDB();
const tx = db.transaction([STORE_NAME], 'readwrite');
const store = tx.objectStore(STORE_NAME);

// Convert Maps to serializable arrays
const clientConsumerArray = Array.from(clientConsumerMap.entries());
const consumerRoutesArray = Array.from(consumerRoutesMap.entries());

store.put(clientConsumerArray, 'clientConsumerMap');
store.put(consumerRoutesArray, 'consumerRoutesMap');

await new Promise((resolve, reject) => {
tx.oncomplete = resolve;
tx.onerror = () => reject(tx.error);
});

db.close();
console.log('[Workerify SW] State saved to IndexedDB');
} catch (error) {
console.error('[Workerify SW] Failed to save state:', error);
}
}

// Load state from IndexedDB
export async function loadState(): Promise<{
clientConsumerMap: Map<string, string>;
consumerRoutesMap: Map<string, Route[]>;
}> {
try {
const db = await initDB();
const tx = db.transaction([STORE_NAME], 'readonly');
const store = tx.objectStore(STORE_NAME);

const clientConsumerRequest = store.get('clientConsumerMap');
const consumerRoutesRequest = store.get('consumerRoutesMap');

const [clientConsumerArray, consumerRoutesArray] = await Promise.all([
new Promise<[string, string][]>((resolve) => {
clientConsumerRequest.onsuccess = () =>
resolve(clientConsumerRequest.result || []);
}),
new Promise<[string, Array<Route>][]>((resolve) => {
consumerRoutesRequest.onsuccess = () =>
resolve(consumerRoutesRequest.result || []);
}),
]);

// Restore Maps from arrays
const clientConsumerMap = new Map(clientConsumerArray);
const consumerRoutesMap = new Map(consumerRoutesArray);

db.close();
console.log('[Workerify SW] State loaded from IndexedDB');
console.log('[Workerify SW] Restored clients:', clientConsumerMap.size);
console.log('[Workerify SW] Restored consumers:', consumerRoutesMap.size);
return {
clientConsumerMap,
consumerRoutesMap,
};
} catch (error) {
console.error('[Workerify SW] Failed to load state:', error);
return {
clientConsumerMap: new Map(),
consumerRoutesMap: new Map(),
};
}
}
Loading
Loading