diff --git a/docs/.vitepress/components.d.ts b/docs/.vitepress/components.d.ts index 19b7059b..0bb67660 100644 --- a/docs/.vitepress/components.d.ts +++ b/docs/.vitepress/components.d.ts @@ -15,7 +15,18 @@ declare module '@vue/runtime-core' { HomePage: typeof import('./theme/components/HomePage.vue')['default'] InjectManifestCleanupOutdatedCaches: typeof import('./theme/components/InjectManifestCleanupOutdatedCaches.md')['default'] InjectManifestSourceMap: typeof import('./theme/components/InjectManifestSourceMap.md')['default'] + PBArticle: typeof import('./theme/components/pwa-builder/PBArticle.vue')['default'] + PBButton: typeof import('./theme/components/pwa-builder/PBButton.vue')['default'] + PBErrors: typeof import('./theme/components/pwa-builder/PBErrors.vue')['default'] + PBForm: typeof import('./theme/components/pwa-builder/PBForm.vue')['default'] + PBInputColor: typeof import('./theme/components/pwa-builder/PBInputColor.vue')['default'] + PBInputRadio: typeof import('./theme/components/pwa-builder/PBInputRadio.vue')['default'] + PBInputText: typeof import('./theme/components/pwa-builder/PBInputText.vue')['default'] + PBRequiredField: typeof import('./theme/components/pwa-builder/PBRequiredField.vue')['default'] + PBResult: typeof import('./theme/components/pwa-builder/PBResult.vue')['default'] + PBResultEntry: typeof import('./theme/components/pwa-builder/PBResultEntry.vue')['default'] PromptForUpdateImg: typeof import('./theme/components/PromptForUpdateImg.vue')['default'] + PWABuilder: typeof import('./theme/components/pwa-builder/PWABuilder.vue')['default'] ReloadPrompt: typeof import('./theme/components/ReloadPrompt.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 41fff717..e2323df1 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -296,15 +296,19 @@ export default defineConfig({ text: 'Examples', link: '/examples/', }, + { + text: 'Deploy', + link: '/deploy/', + }, + { + text: 'Workbox', + link: '/workbox/', + }, ], }, { - text: 'Deploy', - link: '/deployment/', - }, - { - text: 'Workbox', - link: '/workbox/', + text: 'PWA Builder', + link: '/pwa-builder', }, { text: `v${version}`, @@ -326,6 +330,7 @@ export default defineConfig({ '/examples/': prepareSidebar(2), '/deployment/': prepareSidebar(3), '/workbox/': prepareSidebar(4), + '/pwa-builder': prepareSidebar(-1), }, }, }) diff --git a/docs/.vitepress/theme/components/PromptForUpdateImg.vue b/docs/.vitepress/theme/components/PromptForUpdateImg.vue index 0487dbf1..ec369d14 100644 --- a/docs/.vitepress/theme/components/PromptForUpdateImg.vue +++ b/docs/.vitepress/theme/components/PromptForUpdateImg.vue @@ -1,14 +1,12 @@ - diff --git a/docs/.vitepress/theme/components/pwa-builder/PBArticle.vue b/docs/.vitepress/theme/components/pwa-builder/PBArticle.vue new file mode 100644 index 00000000..6ea1ca78 --- /dev/null +++ b/docs/.vitepress/theme/components/pwa-builder/PBArticle.vue @@ -0,0 +1,15 @@ + + + diff --git a/docs/.vitepress/theme/components/pwa-builder/PBButton.vue b/docs/.vitepress/theme/components/pwa-builder/PBButton.vue new file mode 100644 index 00000000..4f9e4878 --- /dev/null +++ b/docs/.vitepress/theme/components/pwa-builder/PBButton.vue @@ -0,0 +1,110 @@ + + + + + diff --git a/docs/.vitepress/theme/components/pwa-builder/PBErrors.vue b/docs/.vitepress/theme/components/pwa-builder/PBErrors.vue new file mode 100644 index 00000000..52c5aaeb --- /dev/null +++ b/docs/.vitepress/theme/components/pwa-builder/PBErrors.vue @@ -0,0 +1,32 @@ + + + diff --git a/docs/.vitepress/theme/components/pwa-builder/PBForm.vue b/docs/.vitepress/theme/components/pwa-builder/PBForm.vue new file mode 100644 index 00000000..91d41698 --- /dev/null +++ b/docs/.vitepress/theme/components/pwa-builder/PBForm.vue @@ -0,0 +1,13 @@ + diff --git a/docs/.vitepress/theme/components/pwa-builder/PBInputColor.vue b/docs/.vitepress/theme/components/pwa-builder/PBInputColor.vue new file mode 100644 index 00000000..8df1627c --- /dev/null +++ b/docs/.vitepress/theme/components/pwa-builder/PBInputColor.vue @@ -0,0 +1,50 @@ + + + diff --git a/docs/.vitepress/theme/components/pwa-builder/PBInputRadio.vue b/docs/.vitepress/theme/components/pwa-builder/PBInputRadio.vue new file mode 100644 index 00000000..9b8324b9 --- /dev/null +++ b/docs/.vitepress/theme/components/pwa-builder/PBInputRadio.vue @@ -0,0 +1,64 @@ + + + diff --git a/docs/.vitepress/theme/components/pwa-builder/PBInputText.vue b/docs/.vitepress/theme/components/pwa-builder/PBInputText.vue new file mode 100644 index 00000000..5bb740b5 --- /dev/null +++ b/docs/.vitepress/theme/components/pwa-builder/PBInputText.vue @@ -0,0 +1,63 @@ + + + diff --git a/docs/.vitepress/theme/components/pwa-builder/PBRequiredField.vue b/docs/.vitepress/theme/components/pwa-builder/PBRequiredField.vue new file mode 100644 index 00000000..75a7675c --- /dev/null +++ b/docs/.vitepress/theme/components/pwa-builder/PBRequiredField.vue @@ -0,0 +1,12 @@ + + + diff --git a/docs/.vitepress/theme/components/pwa-builder/PBResult.vue b/docs/.vitepress/theme/components/pwa-builder/PBResult.vue new file mode 100644 index 00000000..c75dfab8 --- /dev/null +++ b/docs/.vitepress/theme/components/pwa-builder/PBResult.vue @@ -0,0 +1,42 @@ + + + diff --git a/docs/.vitepress/theme/components/pwa-builder/PBResultEntry.vue b/docs/.vitepress/theme/components/pwa-builder/PBResultEntry.vue new file mode 100644 index 00000000..bdcef3d3 --- /dev/null +++ b/docs/.vitepress/theme/components/pwa-builder/PBResultEntry.vue @@ -0,0 +1,45 @@ + + + diff --git a/docs/.vitepress/theme/components/pwa-builder/PWABuilder.vue b/docs/.vitepress/theme/components/pwa-builder/PWABuilder.vue new file mode 100644 index 00000000..6e3c3225 --- /dev/null +++ b/docs/.vitepress/theme/components/pwa-builder/PWABuilder.vue @@ -0,0 +1,208 @@ + + + diff --git a/docs/.vitepress/theme/composables/pwaBuilder.ts b/docs/.vitepress/theme/composables/pwaBuilder.ts new file mode 100644 index 00000000..73a8e922 --- /dev/null +++ b/docs/.vitepress/theme/composables/pwaBuilder.ts @@ -0,0 +1,324 @@ +import { computed, nextTick, onUnmounted, ref } from 'vue' +import { useThrottleFn } from '@vueuse/shared' +import type { + BehaviorType, + BuilderElement, + BuilderError, + FaviconType, + FrameworkType, InjectRegisterType, + PWABuilderData, + RadioData, + State, + StrategyType, + YesNoType, +} from '../types' + +import { DEFAULT_TIMEOUT, generatePWACode, prepareBuilder, resetPWACode } from '../modules/generatePWACode' + +export const focusInput = (element?: HTMLElement, to: ScrollLogicalPosition = 'nearest') => { + setTimeout(() => element?.focus(), 0) + setTimeout(() => element?.scrollIntoView({ + block: to, + behavior: 'smooth', + }), 0) +} + +export const inputs = ref([]) +export const errors = ref([]) + +export function usePWABuilder() { + const state = ref('initial') + const title = ref() + const description = ref() + const shortName = ref() + const themeColor = ref('#ffffff') + const strategy = ref('generateSW') + const behavior = ref('prompt') + const warnUser = ref('false') + const injectRegister = ref(undefined) + const periodicUpdates = ref('false') + const framework = ref(undefined) + const ts = ref(undefined) + const scope = ref('/') + const startUrl = ref(undefined) + const maskedIcon = ref('true') + const favicon = ref('ico') + const cleanupOldAssets = ref('true') + + const generating = ref(false) + + const strategies = createStrategies() + const behaviors = createBehaviors() + const warns = createWarnReady() + const injectRegisters = createInjectRegisters() + const frameworks = createFrameworks() + const yesNoList = createYesNo() + const faviconList = createFavicons() + + const showInjectRegister = computed(() => { + return behavior.value === 'autoUpdate' && warnUser.value === 'false' + }) + + const showTS = computed(() => { + return !(!framework.value || framework.value === 'javascript' || framework.value === 'typescript') + }) + + const generateTypeScript = computed(() => { + return framework.value === 'typescript' || ts.value === 'true' + }) + + const reset = async () => { + title.value = undefined + scope.value = '/' + startUrl.value = undefined + description.value = undefined + shortName.value = undefined + themeColor.value = '#FFFFFF' + maskedIcon.value = 'true' + strategy.value = 'generateSW' + behavior.value = 'prompt' + warnUser.value = 'false' + injectRegister.value = undefined + framework.value = undefined + ts.value = undefined + cleanupOldAssets.value = 'true' + await nextTick() + errors.value.splice(0) + await nextTick() + inputs.value.forEach((i) => { + i.withState(false, i.key === 'title') + }) + } + + const generate = useThrottleFn(async () => { + if (generating.value) + return + + generating.value = true + await nextTick() + errors.value.splice(0) + await nextTick() + try { + const result = await Promise.all(inputs.value.filter((i) => { + switch (i.key) { + case 'title': + case 'scope': + case 'description': + case 'themeColor': + case 'maskedIcon': + case 'framework': + case 'strategy': + case 'periodicUpdates': + case 'behavior': + case 'warn': + case 'cleanupOldAssets': + return true + } + + return false + }).map((i) => { + return i.validate() + })) + const validationResult = result.filter(r => r && r.length > 0).map(r => r[0]) + let customErrors: BuilderError[] | undefined + if (showInjectRegister.value) { + customErrors = await inputs.value.find(i => i.key === 'injectRegister')?.validate() + if (customErrors && customErrors.length > 0) + validationResult.push(customErrors[0]) + } + + if (showTS.value) { + customErrors = await inputs.value.find(i => i.key === 'typescript')?.validate() + if (customErrors && customErrors.length > 0) + validationResult.push(customErrors[0]) + } + + if (validationResult && validationResult.length > 0) { + errors.value.splice(0, errors.value.length, ...validationResult) + await nextTick() + validationResult[0].focus() + return + } + + // reset previous pwa result + resetPWACode() + const data: PWABuilderData = { + title: title.value!, + shortName: shortName.value, + description: description.value!, + themeColor: themeColor.value!, + strategy: strategy.value!, + behavior: behavior.value!, + registerType: injectRegister.value!, + framework: framework.value!, + typescript: generateTypeScript.value, + scope: scope.value!, + startUrl: startUrl.value, + addManifestMaskedIcon: maskedIcon.value === 'true', + favicon: favicon.value!, + cleanupOldAssets: cleanupOldAssets.value === 'true', + periodicSWUpdates: periodicUpdates.value === 'true', + warnsUser: warnUser.value === 'true', + generateFWComponent: behavior.value === 'prompt' || warnUser.value === 'true', + } + // prepare the result: enable entries for target fw + const builder = prepareBuilder(data) + state.value = 'result' + await new Promise(resolve => setTimeout(resolve, DEFAULT_TIMEOUT)) + // generate pwa result + await generatePWACode(builder, data) + } + finally { + generating.value = false + } + }, 256, false, true) + + const back = async () => { + state.value = 'initial' + await nextTick() + generating.value = false + } + + onUnmounted(() => { + inputs.value.splice(0) + }) + + return { + state, + title, + description, + shortName, + themeColor, + strategy, + behavior, + warnUser, + injectRegister, + framework, + ts, + scope, + startUrl, + maskedIcon, + favicon, + showInjectRegister, + periodicUpdates, + showTS, + strategies, + behaviors, + warns, + injectRegisters, + frameworks, + cleanupOldAssets, + yesNoList, + faviconList, + generate, + reset, + generating, + back, + } +} + +function createFrameworks() { + return []>[{ + value: 'javascript', + text: 'Vanilla JavaScript', + disabled: true, + }, { + value: 'typescript', + text: 'TypeScript', + disabled: true, + }, { + value: 'vue', + text: 'Vue 3', + }, { + value: 'react', + text: 'React', + }, { + value: 'preact', + text: 'Preact', + }, { + value: 'svelte', + text: 'Svelte', + }, { + value: 'solid', + text: 'Solid JS', + }, { + value: 'sveltekit', + text: 'SvelteKit', + disabled: true, + }, { + value: 'vitepress', + text: 'VitePress', + disabled: true, + }, { + value: 'iles', + text: 'Îles', + disabled: true, + }, { + value: 'astro', + text: 'Astro (WIP: coming soon)', + disabled: true, + }] +} + +function createStrategies() { + return []>[{ + value: 'generateSW', + text: 'Generate the service worker for me', + }, { + value: 'injectManifest', + text: 'I want to provide my own service worker', + }] +} + +function createBehaviors() { + return []>[{ + value: 'autoUpdate', + text: 'Just auto update my application', + }, { + value: 'prompt', + text: 'I want to ask the user before update', + }] +} + +function createWarnReady() { + return []>[{ + value: 'true', + text: 'Yes, I want to inform user', + }, { + value: 'false', + text: 'No, just keep it as simple as possible', + }] +} + +function createYesNo() { + return []>[{ + value: 'true', + text: 'Yes', + }, { + value: 'false', + text: 'No', + }] +} + +function createInjectRegisters() { + return []>[{ + value: 'inline', + text: 'As simple as possible', + }, { + value: 'script', + text: 'Generate registerSW.js script', + }] +} + +function createFavicons() { + return []>[{ + value: 'ico', + text: 'Only favicon.ico', + }, { + value: 'svg', + text: 'Only favicon.svg', + }, { + value: 'both', + text: 'Both, favicon.ico and favicon.svg', + }] +} diff --git a/docs/.vitepress/theme/composables/useState.ts b/docs/.vitepress/theme/composables/useState.ts new file mode 100644 index 00000000..1666480e --- /dev/null +++ b/docs/.vitepress/theme/composables/useState.ts @@ -0,0 +1,50 @@ +import type { Ref } from 'vue' +import { nextTick, onBeforeMount, ref } from 'vue' +import type { BuilderElement, BuilderError, ValidationResult } from '../types' +import { focusInput, inputs } from './pwaBuilder' + +export function useState(key: string, input: Ref, internalValidation: () => ValidationResult) { + const error = ref(false) + const errors = ref([]) + + const focus = () => { + focusInput(input.value) + } + + const withState = (withError: boolean, focusInput: boolean) => { + error.value = withError + focusInput && focus() + } + + const isValid = () => { + return !error.value + } + + const validate = async () => { + const { isValid, message } = internalValidation() + let internalError: BuilderError | undefined + if (isValid) + errors.value.splice(0) + else + internalError = { key, text: message!, focus } + + if (internalError) + errors.value.splice(0, errors.value.length, internalError) + + await nextTick() + error.value = !isValid + return internalError ? [internalError] : undefined + } + + onBeforeMount(() => { + inputs.value.push({ + key, + focus, + validate, + isValid, + withState, + }) + }) + + return { error, errors, validateField: validate } +} diff --git a/docs/.vitepress/theme/modules/builder/astro.ts b/docs/.vitepress/theme/modules/builder/astro.ts new file mode 100644 index 00000000..787eabcb --- /dev/null +++ b/docs/.vitepress/theme/modules/builder/astro.ts @@ -0,0 +1,12 @@ +import type { PWABuilderData, PWABuilderGenerator } from '../../types' +import { entrypointData, viteConfigData } from '../generatePWACode' + +export default { + configure() { + entrypointData.enabled = true + viteConfigData.enabled = true + }, + generate(data: PWABuilderData) { + return [] + }, +} diff --git a/docs/.vitepress/theme/modules/builder/default.ts b/docs/.vitepress/theme/modules/builder/default.ts new file mode 100644 index 00000000..787eabcb --- /dev/null +++ b/docs/.vitepress/theme/modules/builder/default.ts @@ -0,0 +1,12 @@ +import type { PWABuilderData, PWABuilderGenerator } from '../../types' +import { entrypointData, viteConfigData } from '../generatePWACode' + +export default { + configure() { + entrypointData.enabled = true + viteConfigData.enabled = true + }, + generate(data: PWABuilderData) { + return [] + }, +} diff --git a/docs/.vitepress/theme/modules/builder/iles.ts b/docs/.vitepress/theme/modules/builder/iles.ts new file mode 100644 index 00000000..c6d14c29 --- /dev/null +++ b/docs/.vitepress/theme/modules/builder/iles.ts @@ -0,0 +1,17 @@ +import type { PWABuilderData, PWABuilderGenerator, PWABuilderResultType } from '../../types' +import { ilesConfigData } from '../generatePWACode' + +import { generatePluginConfiguration } from '../plugin' + +export default { + configure({ framework }) { + ilesConfigData.enabled = framework === 'iles' + }, + generate(data: PWABuilderData) { + const generators: [PWABuilderResultType, () => void][] = [] + if (ilesConfigData.enabled) + generators.push(['iles-config', () => generatePluginConfiguration(data, ilesConfigData)]) + + return generators + }, +} diff --git a/docs/.vitepress/theme/modules/builder/javascript.ts b/docs/.vitepress/theme/modules/builder/javascript.ts new file mode 100644 index 00000000..787eabcb --- /dev/null +++ b/docs/.vitepress/theme/modules/builder/javascript.ts @@ -0,0 +1,12 @@ +import type { PWABuilderData, PWABuilderGenerator } from '../../types' +import { entrypointData, viteConfigData } from '../generatePWACode' + +export default { + configure() { + entrypointData.enabled = true + viteConfigData.enabled = true + }, + generate(data: PWABuilderData) { + return [] + }, +} diff --git a/docs/.vitepress/theme/modules/builder/packageJson.ts b/docs/.vitepress/theme/modules/builder/packageJson.ts new file mode 100644 index 00000000..7f4f0ab3 --- /dev/null +++ b/docs/.vitepress/theme/modules/builder/packageJson.ts @@ -0,0 +1,42 @@ +import type { PWABuilderData, PWABuilderGenerator, PWABuilderResultType } from '../../types' +import { packageData } from '../generatePWACode' +import { version } from '../../../../../package.json' + +const ilesVersion = '^0.8.3' +const workboxVersion = '^6.5.3' + +export default { + configure() { + packageData.enabled = true + }, + generate(data: PWABuilderData) { + const generators: [PWABuilderResultType, () => void][] = [ + ['package-json', () => { + const devDependencies: Record = {} + if (data.framework === 'iles') { + devDependencies['@islands/pwa'] = ilesVersion + } + else { + devDependencies['vite-plugin-pwa'] = `^${version}` + if (data.generateFWComponent) { + // include workbox-xxx node only for custom sw + if (data.strategy === 'injectManifest') { + if (data.behavior === 'autoUpdate') + devDependencies['workbox-core'] = workboxVersion + + devDependencies['workbox-precaching'] = workboxVersion + devDependencies['workbox-routing'] = workboxVersion + } + devDependencies['workbox-window'] = workboxVersion + } + } + + packageData.code = ` +// dont forget to install dependencies: npm i, yarn, pnpm i +${JSON.stringify({ devDependencies }, null, 2)} + ` + }], + ] + return generators + }, +} diff --git a/docs/.vitepress/theme/modules/builder/preact.ts b/docs/.vitepress/theme/modules/builder/preact.ts new file mode 100644 index 00000000..4460edfa --- /dev/null +++ b/docs/.vitepress/theme/modules/builder/preact.ts @@ -0,0 +1,40 @@ +import type { PWABuilderData, PWABuilderGenerator, PWABuilderResultType } from '../../types' +import { + dtsConfigData, + entrypointData, + fwCSSComponentData, + tsConfigData, + viteConfigData, +} from '../generatePWACode' +import { generateEntryPoint } from '../entry-point' +import { generatePluginConfiguration } from '../plugin' +import { createJsxGenerators } from '../createJsxGenerators' +import { createTSGenerators } from '../createTSGenerators' + +const assets = import.meta.globEager('/src/assets/preact-*.txt', { as: 'raw' }) +const assetsMap = new Map() +for (const name in assets) { + let assetName = name.slice('/src/assets/preact-'.length) + assetName = assetName.slice(0, assetName.lastIndexOf('.')) + assetsMap.set(assetName, assets[name] as any) +} + +export default { + configure(data) { + entrypointData.enabled = true + viteConfigData.enabled = true + tsConfigData.enabled = data.typescript && data.generateFWComponent + dtsConfigData.enabled = data.generateFWComponent + fwCSSComponentData.enabled = data.generateFWComponent + }, + generate(data: PWABuilderData) { + const generators: [PWABuilderResultType, () => void][] = [ + ['entry-point', () => generateEntryPoint(data, entrypointData)], + ['vite-config', () => generatePluginConfiguration(data, viteConfigData)], + ] + createTSGenerators({ tsConfig: tsConfigData.enabled, dts: dtsConfigData.enabled }, generators) + createJsxGenerators(data, generators, assetsMap) + + return generators + }, +} diff --git a/docs/.vitepress/theme/modules/builder/react.ts b/docs/.vitepress/theme/modules/builder/react.ts new file mode 100644 index 00000000..9f968ffd --- /dev/null +++ b/docs/.vitepress/theme/modules/builder/react.ts @@ -0,0 +1,40 @@ +import type { PWABuilderData, PWABuilderGenerator, PWABuilderResultType } from '../../types' +import { + dtsConfigData, + entrypointData, + fwCSSComponentData, + tsConfigData, + viteConfigData, +} from '../generatePWACode' +import { generateEntryPoint } from '../entry-point' +import { generatePluginConfiguration } from '../plugin' +import { createJsxGenerators } from '../createJsxGenerators' +import { createTSGenerators } from '../createTSGenerators' + +const assets = import.meta.globEager('/src/assets/react-*.txt', { as: 'raw' }) +const assetsMap = new Map() +for (const name in assets) { + let assetName = name.slice('/src/assets/react-'.length) + assetName = assetName.slice(0, assetName.lastIndexOf('.')) + assetsMap.set(assetName, assets[name] as any) +} + +export default { + configure(data) { + entrypointData.enabled = true + viteConfigData.enabled = true + tsConfigData.enabled = data.typescript && data.generateFWComponent + dtsConfigData.enabled = data.generateFWComponent + fwCSSComponentData.enabled = data.generateFWComponent + }, + generate(data: PWABuilderData) { + const generators: [PWABuilderResultType, () => void][] = [ + ['entry-point', () => generateEntryPoint(data, entrypointData)], + ['vite-config', () => generatePluginConfiguration(data, viteConfigData)], + ] + createTSGenerators({ tsConfig: tsConfigData.enabled, dts: dtsConfigData.enabled }, generators) + createJsxGenerators(data, generators, assetsMap) + + return generators + }, +} diff --git a/docs/.vitepress/theme/modules/builder/solid.ts b/docs/.vitepress/theme/modules/builder/solid.ts new file mode 100644 index 00000000..792acf2f --- /dev/null +++ b/docs/.vitepress/theme/modules/builder/solid.ts @@ -0,0 +1,46 @@ +import type { PWABuilderData, PWABuilderGenerator, PWABuilderResultType } from '../../types' +import { + dtsConfigData, + entrypointData, + fwCSSComponentData, + tsConfigData, + viteConfigData, +} from '../generatePWACode' +import { generateEntryPoint } from '../entry-point' +import { generatePluginConfiguration } from '../plugin' +import { createJsxGenerators } from '../createJsxGenerators' +import { createTSGenerators } from '../createTSGenerators' + +const assets = import.meta.globEager('/src/assets/solid-*.txt', { as: 'raw' }) +const assetsMap = new Map() +for (const name in assets) { + let assetName = name.slice('/src/assets/solid-'.length) + assetName = assetName.slice(0, assetName.lastIndexOf('.')) + assetsMap.set(assetName, assets[name] as any) +} + +export default { + configure(data) { + entrypointData.enabled = true + viteConfigData.enabled = true + tsConfigData.enabled = data.typescript && data.generateFWComponent + dtsConfigData.enabled = !data.typescript && data.generateFWComponent + fwCSSComponentData.enabled = data.generateFWComponent + }, + generate(data: PWABuilderData) { + const generators: [PWABuilderResultType, () => void][] = [ + ['entry-point', () => generateEntryPoint(data, entrypointData)], + ['vite-config', () => generatePluginConfiguration(data, viteConfigData)], + ] + createTSGenerators( + { + tsConfig: tsConfigData.enabled, + dts: dtsConfigData.enabled, + }, + generators, + ) + createJsxGenerators(data, generators, assetsMap, 'PWAPrompt.module.css') + + return generators + }, +} diff --git a/docs/.vitepress/theme/modules/builder/svelte.ts b/docs/.vitepress/theme/modules/builder/svelte.ts new file mode 100644 index 00000000..9d8e73d0 --- /dev/null +++ b/docs/.vitepress/theme/modules/builder/svelte.ts @@ -0,0 +1,46 @@ +import type { PWABuilderGenerator, PWABuilderResultType } from '../../types' +import { + dtsConfigData, + entrypointData, + tsConfigData, + viteConfigData, +} from '../generatePWACode' + +import { generateEntryPoint } from '../entry-point' +import { generatePluginConfiguration } from '../plugin' +import { createTSGenerators } from '../createTSGenerators' +import { createPWAPromptGenerators } from '../createPWAPromptGenerator' + +const assets = import.meta.globEager('/src/assets/svelte-*.txt', { as: 'raw' }) +const assetsMap = new Map() +for (const name in assets) { + let assetName = name.slice('/src/assets/svelte-'.length) + assetName = assetName.slice(0, assetName.lastIndexOf('.')) + assetsMap.set(assetName, assets[name] as any) +} + +export default { + configure(data) { + entrypointData.enabled = true + viteConfigData.enabled = true + tsConfigData.enabled = data.typescript && data.generateFWComponent + dtsConfigData.enabled = !data.typescript && data.generateFWComponent + }, + generate(data) { + const generators: [PWABuilderResultType, () => void][] = [ + ['entry-point', () => generateEntryPoint(data, entrypointData)], + ['vite-config', () => generatePluginConfiguration(data, viteConfigData)], + ] + createTSGenerators( + { + tsConfig: tsConfigData.enabled, + dts: dtsConfigData.enabled, + }, + generators, + ) + if (data.generateFWComponent) + createPWAPromptGenerators(data, generators, assetsMap, 'PWAPrompt.svelte', 'svelte', true) + + return generators + }, +} diff --git a/docs/.vitepress/theme/modules/builder/sveltekit.ts b/docs/.vitepress/theme/modules/builder/sveltekit.ts new file mode 100644 index 00000000..787eabcb --- /dev/null +++ b/docs/.vitepress/theme/modules/builder/sveltekit.ts @@ -0,0 +1,12 @@ +import type { PWABuilderData, PWABuilderGenerator } from '../../types' +import { entrypointData, viteConfigData } from '../generatePWACode' + +export default { + configure() { + entrypointData.enabled = true + viteConfigData.enabled = true + }, + generate(data: PWABuilderData) { + return [] + }, +} diff --git a/docs/.vitepress/theme/modules/builder/sw.ts b/docs/.vitepress/theme/modules/builder/sw.ts new file mode 100644 index 00000000..f7479cd8 --- /dev/null +++ b/docs/.vitepress/theme/modules/builder/sw.ts @@ -0,0 +1,163 @@ +import type { PWABuilderData, PWABuilderGenerator, PWABuilderResult, PWABuilderResultType } from '../../types' +import { autoSWData, promptSWData } from '../generatePWACode' + +export default { + configure(data) { + if (data.strategy === 'injectManifest') { + if (data.behavior === 'prompt') + promptSWData.enabled = true + else + autoSWData.enabled = true + } + }, + generate(data: PWABuilderData) { + const generators: [PWABuilderResultType, () => void][] = [] + if (promptSWData.enabled) + generators.push(['prompt-sw', () => generatePromptSW(data, promptSWData)]) + else if (autoSWData.enabled) + generators.push(['auto-sw', () => generateClaimsSW(data, autoSWData)]) + + return generators + }, +} + +function generateClaimsSW( + { + framework, + cleanupOldAssets, + typescript, + }: PWABuilderData, + pwaBuilderResult: PWABuilderResult, +) { + const output = typescript + ? 'src/sw.ts' + : framework === 'sveltekit' ? 'static/sw.js' : 'public/sw.js' + pwaBuilderResult.codeType = typescript ? 'ts' : 'js' + pwaBuilderResult.code = typescript + ? ` +// ${output} + +import { cleanupOutdatedCaches, createHandlerBoundToURL, precacheAndRoute } from 'workbox-precaching' +import { clientsClaim } from 'workbox-core' +import { NavigationRoute, registerRoute } from 'workbox-routing' + +declare let self: ServiceWorkerGlobalScope + +// self.__WB_MANIFEST is default injection point +precacheAndRoute(self.__WB_MANIFEST) + +// clean old assets +${!cleanupOldAssets ? '// ' : ''}cleanupOutdatedCaches() + +let allowlist: undefined | RegExp[] +if (import.meta.env.DEV) + allowlist = [/^\\/$/] + +// to allow work offline +registerRoute(new NavigationRoute( + createHandlerBoundToURL('index.html'), + { allowlist }, +)) + +self.skipWaiting() +clientsClaim() +` + : ` +// ${output} + +import { cleanupOutdatedCaches, createHandlerBoundToURL, precacheAndRoute } from 'workbox-precaching' +import { clientsClaim } from 'workbox-core' +import { NavigationRoute, registerRoute } from 'workbox-routing' + +// self.__WB_MANIFEST is default injection point +precacheAndRoute(self.__WB_MANIFEST) + +// clean old assets +${!cleanupOldAssets ? '// ' : ''}cleanupOutdatedCaches() + +let allowlist = undefined +if (import.meta.env.DEV) + allowlist = [/^\\/$/] + +// to allow work offline +registerRoute(new NavigationRoute( + createHandlerBoundToURL('index.html'), + { allowlist }, +)) + +self.skipWaiting() +clientsClaim() +` +} + +function generatePromptSW( + { + framework, + cleanupOldAssets, + typescript, + }: PWABuilderData, + pwaBuilderResult: PWABuilderResult, +) { + const output = typescript + ? 'src/sw.ts' + : framework === 'sveltekit' ? 'static/sw.js' : 'public/sw.js' + pwaBuilderResult.codeType = typescript ? 'ts' : 'js' + pwaBuilderResult.code = typescript + ? ` +// ${output} + +import { cleanupOutdatedCaches, createHandlerBoundToURL, precacheAndRoute } from 'workbox-precaching' +import { NavigationRoute, registerRoute } from 'workbox-routing' + +declare let self: ServiceWorkerGlobalScope + +self.addEventListener('message', (event) => { + if (event.data && event.data.type === 'SKIP_WAITING') + self.skipWaiting() +}) + +// self.__WB_MANIFEST is default injection point +precacheAndRoute(self.__WB_MANIFEST) + +// clean old assets +${!cleanupOldAssets ? '// ' : ''}cleanupOutdatedCaches() + +let allowlist: undefined | RegExp[] +if (import.meta.env.DEV) + allowlist = [/^\\/$/] + +// to allow work offline +registerRoute(new NavigationRoute( + createHandlerBoundToURL('index.html'), + { allowlist }, +)) +` + : ` +// ${output} + +import { cleanupOutdatedCaches, createHandlerBoundToURL, precacheAndRoute } from 'workbox-precaching' +import { NavigationRoute, registerRoute } from 'workbox-routing' + +self.addEventListener('message', (event) => { + if (event.data && event.data.type === 'SKIP_WAITING') + self.skipWaiting() +}) + +// self.__WB_MANIFEST is default injection point +precacheAndRoute(self.__WB_MANIFEST) + +// clean old assets +${!cleanupOldAssets ? '// ' : ''}cleanupOutdatedCaches() + +let allowlist = undefined +if (import.meta.env.DEV) + allowlist = [/^\\/$/] + +// to allow work offline +registerRoute(new NavigationRoute( + createHandlerBoundToURL('index.html'), + { allowlist }, +)) +` +} + diff --git a/docs/.vitepress/theme/modules/builder/typescript.ts b/docs/.vitepress/theme/modules/builder/typescript.ts new file mode 100644 index 00000000..787eabcb --- /dev/null +++ b/docs/.vitepress/theme/modules/builder/typescript.ts @@ -0,0 +1,12 @@ +import type { PWABuilderData, PWABuilderGenerator } from '../../types' +import { entrypointData, viteConfigData } from '../generatePWACode' + +export default { + configure() { + entrypointData.enabled = true + viteConfigData.enabled = true + }, + generate(data: PWABuilderData) { + return [] + }, +} diff --git a/docs/.vitepress/theme/modules/builder/vitepress.ts b/docs/.vitepress/theme/modules/builder/vitepress.ts new file mode 100644 index 00000000..787eabcb --- /dev/null +++ b/docs/.vitepress/theme/modules/builder/vitepress.ts @@ -0,0 +1,12 @@ +import type { PWABuilderData, PWABuilderGenerator } from '../../types' +import { entrypointData, viteConfigData } from '../generatePWACode' + +export default { + configure() { + entrypointData.enabled = true + viteConfigData.enabled = true + }, + generate(data: PWABuilderData) { + return [] + }, +} diff --git a/docs/.vitepress/theme/modules/builder/vue.ts b/docs/.vitepress/theme/modules/builder/vue.ts new file mode 100644 index 00000000..ee1e4126 --- /dev/null +++ b/docs/.vitepress/theme/modules/builder/vue.ts @@ -0,0 +1,30 @@ +import type { PWABuilderData, PWABuilderGenerator, PWABuilderResultType } from '../../types' +import { entrypointData, viteConfigData } from '../generatePWACode' +import { generateEntryPoint } from '../entry-point' +import { generatePluginConfiguration } from '../plugin' +import { createPWAPromptGenerators } from '../createPWAPromptGenerator' + +const assets = import.meta.globEager('/src/assets/vue-*.txt', { as: 'raw' }) +const assetsMap = new Map() +for (const name in assets) { + let assetName = name.slice('/src/assets/vue-'.length) + assetName = assetName.slice(0, assetName.lastIndexOf('.')) + assetsMap.set(assetName, assets[name] as any) +} + +export default { + configure() { + entrypointData.enabled = true + viteConfigData.enabled = true + }, + generate(data: PWABuilderData) { + const generators: [PWABuilderResultType, () => void][] = [ + ['entry-point', () => generateEntryPoint(data, entrypointData)], + ['vite-config', () => generatePluginConfiguration(data, viteConfigData)], + ] + if (data.generateFWComponent) + createPWAPromptGenerators(data, generators, assetsMap, 'PWAPrompt.vue', 'vue', true) + + return generators + }, +} diff --git a/docs/.vitepress/theme/modules/createJsxGenerators.ts b/docs/.vitepress/theme/modules/createJsxGenerators.ts new file mode 100644 index 00000000..056d83b7 --- /dev/null +++ b/docs/.vitepress/theme/modules/createJsxGenerators.ts @@ -0,0 +1,15 @@ +import type { PWABuilderData } from '../types' +import { fwCSSComponentData } from './generatePWACode' +import { createPWAPromptGenerators } from './createPWAPromptGenerator' + +export function createJsxGenerators(data: PWABuilderData, generators: any[], assetsMap: Map, cssName = 'PWAPrompt.css') { + if (data.generateFWComponent) { + createPWAPromptGenerators(data, generators, assetsMap) + generators.push(['prompt-css', () => { + fwCSSComponentData.code = ` +/* ${cssName} */ +${assetsMap.get('sfc-style')} +` + }]) + } +} diff --git a/docs/.vitepress/theme/modules/createPWAPromptGenerator.ts b/docs/.vitepress/theme/modules/createPWAPromptGenerator.ts new file mode 100644 index 00000000..9e0a5845 --- /dev/null +++ b/docs/.vitepress/theme/modules/createPWAPromptGenerator.ts @@ -0,0 +1,59 @@ +import type { CodeType, PWABuilderData } from '../types' + +import { fwComponentData } from './generatePWACode' + +export function createPWAPromptGenerators( + data: PWABuilderData, + generators: any[], + assetsMap: Map, + name?: string, + codeType?: CodeType, + addCss = false, +) { + if (data.generateFWComponent) { + const { behavior, warnsUser, periodicSWUpdates, typescript } = data + generators.push(['prompt-component', () => { + fwComponentData.codeType = codeType ?? (typescript ? 'tsx' : 'jsx') + let template: string + if (behavior === 'autoUpdate') { + template = periodicSWUpdates ? 'sfc-warn-updates' : 'sfc-warn' + } + else { + template = warnsUser + ? (periodicSWUpdates ? 'sfc-prompt-warn-updates' : 'sfc-prompt-warn') + : (periodicSWUpdates ? 'sfc-prompt-updates' : 'sfc-prompt') + } + + const useName = name ?? `PWAPrompt.${typescript ? 't' : 'j'}sx` + + if (addCss) { + fwComponentData.code = ` +/* ${useName} */ +${assetsMap.get(template)} +${assetsMap.get('sfc-style')} +` + } + else { + fwComponentData.code = ` +/* ${useName} */ +${assetsMap.get(template)} +` + } + + if (data.typescript) { + switch (data.framework) { + case 'vue': + fwComponentData.code = fwComponentData.code!.replace(' + +{#if toast} + +{/if} diff --git a/docs/src/assets/svelte-sfc-prompt-warn-updates.txt b/docs/src/assets/svelte-sfc-prompt-warn-updates.txt new file mode 100644 index 00000000..9729c5e5 --- /dev/null +++ b/docs/src/assets/svelte-sfc-prompt-warn-updates.txt @@ -0,0 +1,53 @@ + + +{#if toast} + +{/if} diff --git a/docs/src/assets/svelte-sfc-prompt-warn.txt b/docs/src/assets/svelte-sfc-prompt-warn.txt new file mode 100644 index 00000000..6bb0cedd --- /dev/null +++ b/docs/src/assets/svelte-sfc-prompt-warn.txt @@ -0,0 +1,50 @@ + + +{#if toast} + +{/if} diff --git a/docs/src/assets/svelte-sfc-prompt.txt b/docs/src/assets/svelte-sfc-prompt.txt new file mode 100644 index 00000000..d488fa7a --- /dev/null +++ b/docs/src/assets/svelte-sfc-prompt.txt @@ -0,0 +1,40 @@ + + +{#if toast} + +{/if} diff --git a/docs/src/assets/svelte-sfc-style.txt b/docs/src/assets/svelte-sfc-style.txt new file mode 100644 index 00000000..9942650d --- /dev/null +++ b/docs/src/assets/svelte-sfc-style.txt @@ -0,0 +1,25 @@ + diff --git a/docs/src/assets/svelte-sfc-warn-updates.txt b/docs/src/assets/svelte-sfc-warn-updates.txt new file mode 100644 index 00000000..ba351715 --- /dev/null +++ b/docs/src/assets/svelte-sfc-warn-updates.txt @@ -0,0 +1,39 @@ + + +{#if toast} + +{/if} diff --git a/docs/src/assets/svelte-sfc-warn.txt b/docs/src/assets/svelte-sfc-warn.txt new file mode 100644 index 00000000..e6c88255 --- /dev/null +++ b/docs/src/assets/svelte-sfc-warn.txt @@ -0,0 +1,36 @@ + + +{#if toast} + +{/if} diff --git a/docs/src/assets/vue-sfc-prompt-updates.txt b/docs/src/assets/vue-sfc-prompt-updates.txt new file mode 100644 index 00000000..03744558 --- /dev/null +++ b/docs/src/assets/vue-sfc-prompt-updates.txt @@ -0,0 +1,41 @@ + + + diff --git a/docs/src/assets/vue-sfc-prompt-warn-updates.txt b/docs/src/assets/vue-sfc-prompt-warn-updates.txt new file mode 100644 index 00000000..4806e153 --- /dev/null +++ b/docs/src/assets/vue-sfc-prompt-warn-updates.txt @@ -0,0 +1,45 @@ + + + diff --git a/docs/src/assets/vue-sfc-prompt-warn.txt b/docs/src/assets/vue-sfc-prompt-warn.txt new file mode 100644 index 00000000..4a206975 --- /dev/null +++ b/docs/src/assets/vue-sfc-prompt-warn.txt @@ -0,0 +1,42 @@ + + + diff --git a/docs/src/assets/vue-sfc-prompt.txt b/docs/src/assets/vue-sfc-prompt.txt new file mode 100644 index 00000000..ca39fd5a --- /dev/null +++ b/docs/src/assets/vue-sfc-prompt.txt @@ -0,0 +1,38 @@ + + + diff --git a/docs/src/assets/vue-sfc-style.txt b/docs/src/assets/vue-sfc-style.txt new file mode 100644 index 00000000..1be9e1f0 --- /dev/null +++ b/docs/src/assets/vue-sfc-style.txt @@ -0,0 +1,24 @@ + diff --git a/docs/src/assets/vue-sfc-warn-updates.txt b/docs/src/assets/vue-sfc-warn-updates.txt new file mode 100644 index 00000000..c4e6428e --- /dev/null +++ b/docs/src/assets/vue-sfc-warn-updates.txt @@ -0,0 +1,38 @@ + + + diff --git a/docs/src/assets/vue-sfc-warn.txt b/docs/src/assets/vue-sfc-warn.txt new file mode 100644 index 00000000..caa80a9c --- /dev/null +++ b/docs/src/assets/vue-sfc-warn.txt @@ -0,0 +1,35 @@ + + + diff --git a/docs/vite.config.ts b/docs/vite.config.ts index 188bc66b..8213a62e 100644 --- a/docs/vite.config.ts +++ b/docs/vite.config.ts @@ -1,6 +1,6 @@ import { defineConfig } from 'vite' import Components from 'unplugin-vue-components/vite' -import { presetAttributify, presetUno } from 'unocss' +import { presetAttributify, presetIcons, presetUno } from 'unocss' import Unocss from 'unocss/vite' import { VitePWA } from '../dist' import NavbarFix from './plugins/navbar' @@ -42,7 +42,29 @@ export default defineConfig({ // https://github.com/unocss/unocss Unocss({ - presets: [presetUno(), presetAttributify()], + theme: { + breakpoints: { + 'xs': '468px', + 'sm': '640px', + 'md': '768px', + 'lg': '1024px', + 'xl': '1280px', + '2xl': '1536px', + }, + }, + shortcuts: [ + { 'pb-input': 'grid grid-cols-[150px_1fr] gap-x-1rem items-baseline lt-sm:grid-cols-[1fr]' }, + { 'pb-error': 'animate-shake-x animate-count-1 animate-delay-0.5s animate-duration-1s' }, + { 'pb-input-enter': 'animate-zoom-in animate-count-1 animate-duration-0.5s' }, + { 'pb-input-leave': 'animate-zoom-out animate-count-1 animate-duration-0.3s' }, + { 'pb-errors-enter': 'animate-zoom-in animate-count-1 animate-duration-0.5s' }, + { 'pb-errors-leave': 'animate-zoom-out animate-count-1 animate-duration-0.3s' }, + ], + presets: [ + presetIcons(), + presetUno(), + presetAttributify(), + ], }), // https://github.com/antfu/vite-plugin-pwa diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f06a0c88..a8026c08 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -61,6 +61,7 @@ importers: docs: specifiers: + '@iconify-json/line-md': ^1.1.13 '@types/fs-extra': ^9.0.13 '@vitejs/plugin-vue': ^2.3.3 '@vueuse/core': ^8.7.5 @@ -83,6 +84,7 @@ importers: '@vueuse/shared': 8.7.5_vue@3.2.37 vue: 3.2.37 devDependencies: + '@iconify-json/line-md': 1.1.13 '@types/fs-extra': 9.0.13 '@vitejs/plugin-vue': 2.3.3_vite@2.9.13+vue@3.2.37 esbuild-register: 3.3.3 @@ -2318,6 +2320,12 @@ packages: resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} dev: true + /@iconify-json/line-md/1.1.13: + resolution: {integrity: sha512-LEyIQBAb4wdSnWayI+RuX9aIDZcpnlak4Zz9sBUR59u7Tcz7aVQcNpIzlS1YbZlufWgh84cs+0F9PfNYOacoZg==} + dependencies: + '@iconify/types': 1.1.0 + dev: true + /@iconify/types/1.1.0: resolution: {integrity: sha512-Jh0llaK2LRXQoYsorIH8maClebsnzTcve+7U3rQUSnC11X4jtPnFuyatqFLvMxZ8MLG8dB4zfHsbPfuvxluONw==} dev: true