Skip to content
Commits on Source (28)
......@@ -6,5 +6,22 @@ module.exports = {
},
extends: [
'standard'
],
ignorePatterns: [
'dist'
],
reportUnusedDisableDirectives: true,
overrides: [
{
files: ['src/**/*.ts'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking'
],
parserOptions: {
project: true,
tsconfigRootDir: __dirname
}
}
]
}
node_modules
dist
output
.eslintcache
......@@ -2,11 +2,20 @@ include:
- project: 'sre/ci-building-blocks'
file: 'nodejs.yml'
audit npm modules:
extends: .audit npm modules
timeout: 5 minutes
tags:
- tiny-hetzner
unit tests:
extends:
- .unit tests
variables:
KUBERNETES_CPU_REQUEST: 2
KUBERNETES_CPU_LIMIT: 3
KUBERNETES_MEMORY_REQUEST: 2Gi
KUBERNETES_MEMORY_LIMIT: 2Gi
extends: .unit tests
timeout: 10 minutes
tags:
- build-hetzner
eslint:
extends: .eslint
tags:
- build-hetzner
timeout: 10 minutes
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged
npx --yes lint-staged
.husky
.gitlab
.gitlab-ci.yml
src
spec
test
output
tsconfig.json
jest.config.*
.eslint*
.editorconfig
......@@ -4,6 +4,17 @@ All notable changes to this project will be documented in this file.
## [Unreleased]
## [0.6.0] - 2023-05-16
### Changed
- Convert source code to TypeScript [`029a4c7`](https://gitlab.open-xchange.com/frontend/vite-plugin-ox-manifests/commit/029a4c75c96483a221a54fc54589467b1a77eead)
### Removed
- Coverage files from npm package [`11fc7a8`](https://gitlab.open-xchange.com/frontend/vite-plugin-ox-manifests/commit/11fc7a8ba825f1a3bf1913b0134b57b1d1029f99)
## [0.5.4] - 2023-03-17
### Removed
......@@ -22,6 +33,7 @@ All notable changes to this project will be documented in this file.
- Start Changelog
[unreleased]: https://gitlab.open-xchange.com/frontend/vite-plugin-ox-manifests/compare/0.5.4...main
[unreleased]: https://gitlab.open-xchange.com/frontend/vite-plugin-ox-manifests/compare/0.6.0...main
[0.6.0]: https://gitlab.open-xchange.com/frontend/vite-plugin-ox-manifests/compare/0.5.4...0.6.0
[0.5.4]: https://gitlab.open-xchange.com/frontend/vite-plugin-ox-manifests/compare/0.5.3...0.5.4
[0.5.3]: https://gitlab.open-xchange.com/frontend/vite-plugin-ox-manifests/compare/0.5.2...0.5.3
import { PROJECT_NAME as name } from './src/constants.js'
import manifestsPlugin from './src/plugins/manifests.js'
import relativePathsPlugin from './src/plugins/relative-paths.js'
import settingsPlugin from './src/plugins/settings.js'
import servePlugin from './src/plugins/serve.js'
import gettextPlugin from './src/plugins/gettext.js'
import metaPlugin from './src/plugins/meta.js'
import { deepMergeObject } from './src/util.js'
/**
* Creates a vite-plugin to include manifests in dev and production mode
*
* @param {object} [options]
* @param {boolean} [options.watch=false] - If set to true, this plugin will watch manifest changes and reload the ui
* @param {string} [options.entryPoints] - Convenience method to specify additional entry points for the production build. Can be specified as a glob-pattern.
* @param {boolean} [options.manifestsAsEntryPoints=true] - Specifies, that every path in a manifest is used as an entry-point for the production build.
* @param {boolean} [options.autoloadSettings=true] - If disabled, settings will not be automatically detected and therefore have to provide a manual manifest.json.
* @param supportedEntryExtensions {string[]=['js', 'mjs', 'ts']} - An array of valid file-extensions for entry points.
* @param {object} [options.meta] - An object that will be translated into a meta.json file in the root directory.
*/
export default function ({
watch = false,
entryPoints,
manifestsAsEntryPoints = true,
transformAbsolutePaths,
autoloadSettings = true,
supportedEntryExtensions = ['js', 'mjs', 'ts'],
meta
} = {}) {
const options = {
watch,
entryPoints,
manifestsAsEntryPoints,
transformAbsolutePaths,
autoloadSettings,
supportedEntryExtensions,
meta
}
const plugins = [
// add settings first
settingsPlugin(options),
relativePathsPlugin(options),
servePlugin(options),
gettextPlugin(options),
metaPlugin(options),
// manifest plugin last
manifestsPlugin(options)
]
let resolvedConfig
const vitePluginOxManifests = {
name,
enforce: 'post',
async config (config, env) {
for (const plugin of plugins) {
if (plugin.config) {
config = (await plugin.config.call(this, config, env)) || config
}
}
return config
},
async getManifests () {
const manifestsByHash = {}
for (const plugin of plugins) {
if (plugin.getManifests) {
const manifests = await plugin.getManifests.call(this)
manifests.forEach(manifest => {
const hash = `${manifest.namespace}--${manifest.path}`
// overwrites existing ones. last wins
manifestsByHash[hash] = manifest
})
}
}
return Object.values(manifestsByHash)
},
async configResolved (config) {
resolvedConfig = config
for (const plugin of plugins) {
if (plugin.configResolved) {
plugin._config = config
await plugin.configResolved.call(this, config)
}
}
},
async options (options) {
if (resolvedConfig.mode === 'production') {
options.preserveEntrySignatures = 'strict'
}
for (const plugin of plugins) {
if (plugin.options) {
options = (await plugin.options.call(this, options)) || options
}
}
return options
},
buildStart (options) {
for (const plugin of plugins) {
if (plugin.buildStart) {
plugin.buildStart.call(this, options)
}
}
},
async configureServer (server) {
for (const plugin of plugins) {
if (plugin.configureServer) {
await plugin.configureServer.call(vitePluginOxManifests, server)
}
}
},
async resolveId (source, importer, options) {
for (const plugin of plugins) {
if (plugin.resolveId) {
const result = await plugin.resolveId.call(this, source, importer, options)
if (result !== undefined) return result
}
}
},
async load (id) {
for (const plugin of plugins) {
if (plugin.load) {
const result = await plugin.load.call(this, id)
if (result !== undefined) return result
}
}
},
async transform (code, id) {
let result = {}
for (const plugin of plugins) {
if (plugin.transform) {
// TODO check what happens if there is already meta from other plugins
const that = await plugin.transform.call(this, code, id)
if (that !== undefined) {
result = deepMergeObject(result, that)
}
}
}
if (Object.keys(result).length === 0) return
return result
},
async generateBundle (options, bundle) {
for (const plugin of plugins) {
if (plugin.generateBundle) {
await plugin.generateBundle.call(this, options, bundle)
}
}
},
async transformIndexHtml (src) {
for (const plugin of plugins) {
if (plugin.transformIndexHtml) {
src = (await plugin.transformIndexHtml.call(this, src)) || src
}
}
return src
}
}
return vitePluginOxManifests
}
export default {
collectCoverage: false,
coverageDirectory: 'output/coverage',
coverageProvider: 'v8',
coverageReporters: [
'text',
'cobertura',
'text-summary'
],
extensionsToTreatAsEsm: ['.ts'],
reporters: [
'default',
['jest-junit', { outputDirectory: 'output/' }]
],
transform: {
'^.+\\.ts$': ['ts-jest', { useESM: true }]
}
}
{
"name": "@open-xchange/vite-plugin-ox-manifests",
"version": "0.5.4",
"version": "0.6.0",
"description": "A vite plugin to concat and serve ox manifests",
"main": "index.js",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"type": "module",
"repository": "git@gitlab.open-xchange.com:frontend/vite-plugin-ox-manifests.git",
"author": "Richard Petersen",
"license": "MIT",
"scripts": {
"lint": "eslint *.js src/**/*.js test/**/**.js",
"prepare": "husky install",
"test": "NODE_OPTIONS=\"--experimental-vm-modules --max_old_space_size=1792\" jest test/**/*.test.js --testTimeout=15000 --runInBand"
"lint": "tsc --noEmit && eslint . --ext .js,.cjs,.mjs,.ts",
"build": "npx --yes rimraf dist && tsc",
"test": "cross-env NODE_OPTIONS=\"--experimental-vm-modules --max_old_space_size=1792\" jest --testTimeout=15000 --runInBand"
},
"dependencies": {
"@open-xchange/rollup-plugin-po2json": "^0.7.1",
"@open-xchange/rollup-plugin-po2json": "^0.8.0",
"chokidar": "^3.5.1",
"fast-glob": "^3.2.12",
"parseurl": "^1.3.3",
"vite": "^4.0.2"
"magic-string": "^0.30.0",
"parseurl": "^1.3.3"
},
"devDependencies": {
"@open-xchange/lint": "^0.0.2",
"@open-xchange/vite-plugin-ox-externals": "^0.4.1",
"@open-xchange/vite-plugin-ox-externals": "^0.5.0",
"@types/node": "^20.1.5",
"@types/parseurl": "^1.3.1",
"@typescript-eslint/eslint-plugin": "^5.59.0",
"@typescript-eslint/parser": "^5.59.0",
"cross-env": "^7.0.3",
"eslint-plugin-jest": "^27.2.1",
"express": "^4.18.2",
"jest": "^29.5.0",
"jest-junit": "^16.0.0",
"less": "^4.1.3",
"rollup": "^3.19.1"
"ts-jest": "^29.1.0",
"typescript": "^5.0.4",
"vite": "^4.2.1"
},
"lint-staged": {
"*.js": "eslint --cache --fix"
"*.{js,cjs,mjs,ts}": "eslint --cache --fix"
}
}
export const PROJECT_NAME = '@open-xchange/vite-plugin-ox-manifests'
import type { Plugin, ResolvedConfig, Rollup } from 'vite'
import type { VitePluginOxManifestsPlugin } from './plugins/plugin'
import { deepMergeObject, mergeManifests } from './util'
import manifestsPlugin from './plugins/manifests'
import relativePathsPlugin from './plugins/relative-paths'
import settingsPlugin from './plugins/settings'
import servePlugin from './plugins/serve'
import gettextPlugin from './plugins/gettext'
import metaPlugin from './plugins/meta'
export { mergeManifests }
export interface OxManifest {
namespace: string
path: string
requires?: string
raw?: string
generator?: string
}
/**
* Configuration options for the Vite plugin "vite-plugin-ox-manifests".
*/
export interface VitePluginOxManifestsOptions {
/**
* If set to `true`, this plugin will watch manifest changes and reload the
* UI. Default value is `false`.
*/
watch?: boolean
/**
* Convenience method to specify additional entry points for the production
* build. Can be specified as a glob-pattern.
*/
entryPoints?: string
/**
* Specifies, that every path in a manifest is used as an entry-point for the
* production build. Default is `true`!
*/
manifestsAsEntryPoints?: boolean
/**
* If disabled, settings will not be automatically detected and therefore
* have to provide a manual manifest.json. Default is `true`!
*/
autoloadSettings?: boolean
/**
* An array of valid file-extensions for entry points. Default is
* `['js', 'mjs', 'ts']`.
*/
supportedEntryExtensions?: string[]
/**
* An object that will be translated into a `meta.json` file in the root
* directory.
*/
meta?: object | null
}
/**
* Type shape of the Vite plugin "vite-plugin-ox-manifests".
*/
export interface VitePluginOxManifests extends Plugin {
getManifests(): Promise<OxManifest[]>
}
export const PROJECT_NAME = '@open-xchange/vite-plugin-ox-manifests'
/**
* Creates a vite-plugin to include manifests in dev and production mode
*/
export default function pluginOxManifests (options?: VitePluginOxManifestsOptions): VitePluginOxManifests {
const resolvedOptions: Required<VitePluginOxManifestsOptions> = {
watch: false,
entryPoints: '',
manifestsAsEntryPoints: true,
autoloadSettings: true,
supportedEntryExtensions: ['js', 'mjs', 'ts'],
meta: null,
...options
}
let resolvedConfig: ResolvedConfig
const plugins: VitePluginOxManifestsPlugin[] = [
// add settings first
settingsPlugin(resolvedOptions),
relativePathsPlugin(resolvedOptions),
servePlugin(resolvedOptions),
gettextPlugin(resolvedOptions),
metaPlugin(resolvedOptions),
// manifest plugin last
manifestsPlugin(resolvedOptions)
]
const pluginManifests: VitePluginOxManifests = {
name: PROJECT_NAME,
enforce: 'post',
async config (config, env) {
for (const plugin of plugins) {
plugin.pluginResolved?.(pluginManifests)
if (typeof plugin.config === 'function') {
config = (await plugin.config.call(this, config, env)) || config
} else if (plugin.config) {
throw new TypeError(`object hooks not supported, specify function callback (${plugin.name}.config)`)
}
}
return config
},
async getManifests () {
const manifests: OxManifest[] = []
for (const plugin of plugins) {
if (plugin.getManifests) {
const result = await plugin.getManifests.call(this)
manifests.push(...result)
}
}
return mergeManifests(manifests)
},
async configResolved (config) {
resolvedConfig = config
for (const plugin of plugins) {
if (typeof plugin.configResolved === 'function') {
await plugin.configResolved.call(this, config)
} else if (plugin.configResolved) {
throw new TypeError(`object hooks not supported, specify function callback (${plugin.name}.configResolved)`)
}
}
},
async options (options) {
if (resolvedConfig.mode === 'production') {
options.preserveEntrySignatures = 'strict'
}
for (const plugin of plugins) {
if (typeof plugin.options === 'function') {
options = (await plugin.options.call(this, options)) || options
} else if (plugin.options) {
throw new TypeError(`${plugin.name}.options: object hooks not supported, specify function callback`)
}
}
return options
},
async buildStart (options) {
for (const plugin of plugins) {
if (typeof plugin.buildStart === 'function') {
await plugin.buildStart.call(this, options)
} else if (plugin.buildStart) {
throw new TypeError(`${plugin.name}.buildStart: object hooks not supported, specify function callback`)
}
}
},
async configureServer (server) {
for (const plugin of plugins) {
if (typeof plugin.configureServer === 'function') {
await plugin.configureServer.call(this, server)
} else if (plugin.configureServer) {
throw new TypeError(`${plugin.name}.configureServer: object hooks not supported, specify function callback`)
}
}
},
async resolveId (source, importer, options) {
for (const plugin of plugins) {
if (typeof plugin.resolveId === 'function') {
const result = await plugin.resolveId.call(this, source, importer, options)
if (result !== undefined) return result
} else if (plugin.resolveId) {
throw new TypeError(`${plugin.name}.resolveId: object hooks not supported, specify function callback`)
}
}
return undefined
},
async load (id) {
for (const plugin of plugins) {
if (typeof plugin.load === 'function') {
const result = await plugin.load.call(this, id)
if (result !== undefined) return result
} else if (plugin.load) {
throw new TypeError(`${plugin.name}.load: object hooks not supported, specify function callback`)
}
}
return undefined
},
async transform (code, id) {
let result: Partial<Rollup.SourceDescription> = {}
for (const plugin of plugins) {
if (typeof plugin.transform === 'function') {
// TODO check what happens if there is already meta from other plugins
const that = await plugin.transform.call(this, code, id)
if (that && typeof that === 'object') {
result = deepMergeObject(result, that)
}
} else if (plugin.transform) {
throw new TypeError(`${plugin.name}.transform: object hooks not supported, specify function callback`)
}
}
return Object.keys(result).length ? result : undefined
},
async generateBundle (options, bundle, isWrite) {
for (const plugin of plugins) {
if (typeof plugin.generateBundle === 'function') {
await plugin.generateBundle.call(this, options, bundle, isWrite)
} else if (plugin.generateBundle) {
throw new TypeError(`${plugin.name}.generateBundle: object hooks not supported, specify function callback`)
}
}
},
async transformIndexHtml (src, ctx) {
for (const plugin of plugins) {
if (typeof plugin.transformIndexHtml === 'function') {
const result = await plugin.transformIndexHtml.call(this, src, ctx)
if (typeof result === 'string') {
src = result
} else if (result) {
throw new TypeError(`${plugin.name}.transformIndexHtml: tag descriptors not supported, return simple string`)
}
} else if (plugin.transformIndexHtml) {
throw new TypeError(`${plugin.name}.transformIndexHtml: object hooks not supported, specify function callback`)
}
}
return src
}
}
return pluginManifests
}
import { applyInputToOptions } from '../util.js'
import path, { posix } from 'path'
import { readdir } from 'fs/promises'
import { normalizePath } from 'vite'
import { parsePoFile, namespacesFrom } from '@open-xchange/rollup-plugin-po2json/lib/po2json.js'
export default function () {
const manifests = []
let resolvedConfig
return {
configResolved (config) {
resolvedConfig = config
},
async options (options) {
const gettextPlugin = resolvedConfig.plugins.find(plugin => plugin.name === 'gettext')
if (!gettextPlugin) return
const { meta: { poFiles, defaultDictionary, defaultLanguage } } = gettextPlugin
const input = {}
// add dictionaries as entrypoints
if (typeof poFiles === 'string') {
const poDir = path.dirname(poFiles)
const files = (await readdir(poDir)).filter(f => f.indexOf(defaultLanguage) >= 0).map(f => normalizePath(`${poDir}/${f}`))
await Promise.all(files.map(async (file) => {
const po = await parsePoFile(file)
for (const namespace of (namespacesFrom(po.items))) {
if (namespace === defaultDictionary) continue
input[namespace] = `gettext?dictionary=${namespace}`
manifests.push({ namespace: 'i18n', path: `${namespace}` })
}
}))
}
input[defaultDictionary] = 'gettext'
manifests.push({ namespace: 'i18n', path: `${defaultDictionary}` })
if (resolvedConfig.mode === 'production') {
applyInputToOptions(input, options)
}
return options
},
getManifests () {
if (resolvedConfig.mode !== 'production') {
return manifests.map(({ namespace, path }) => ({ namespace, raw: posix.join(resolvedConfig.base, `/@id/${path}.js`), path }))
}
return manifests
},
async generateBundle (options, bundle) {
for (const file in bundle) {
const chunk = bundle[file]
if (!chunk.facadeModuleId) continue
const modules = Object.keys(chunk.modules)
if (modules.length === 0) modules.push(chunk.facadeModuleId)
modules.forEach(id => {
const meta = this.getModuleInfo(id)?.meta
if (meta?.gettext?.dictionary !== true) return
meta.manifests = meta.manifests || []
meta.manifests.push({ namespace: 'i18n' })
})
}
}
}
}
import { dirname, posix } from 'node:path'
import { readdir } from 'node:fs/promises'
import { type ResolvedConfig, normalizePath } from 'vite'
import type { GettextPlugin } from '@open-xchange/rollup-plugin-po2json'
import { PROJECT_NAME as GETTEXT_PROJECT_NAME, parsePoFile, namespacesFrom } from '@open-xchange/rollup-plugin-po2json'
import { type VitePluginOxManifestsModuleMeta, definePlugin } from './plugin'
import { type Dict, applyInputToOptions } from '../util'
import { type OxManifest, PROJECT_NAME } from '../index'
export default definePlugin(() => {
const manifests: OxManifest[] = []
let resolvedConfig: ResolvedConfig
return {
name: `${PROJECT_NAME}/gettext-plugin`,
configResolved (config) {
resolvedConfig = config
},
async options (options) {
const gettextPlugin = resolvedConfig.plugins.find(plugin => plugin.name === GETTEXT_PROJECT_NAME)
if (!gettextPlugin) return
const { meta: { poFiles, defaultDictionary, defaultLanguage } } = gettextPlugin as GettextPlugin
const input: Dict<string> = {}
// add dictionaries as entry points
const poDir = dirname(poFiles)
const files = (await readdir(poDir)).filter(f => f.indexOf(defaultLanguage) >= 0).map(f => normalizePath(`${poDir}/${f}`))
await Promise.all(files.map(async (file) => {
const po = await parsePoFile(file)
for (const namespace of namespacesFrom(po.items)) {
if (namespace !== defaultDictionary) {
input[namespace] = `gettext?dictionary=${namespace}`
manifests.push({ namespace: 'i18n', path: namespace })
}
}
}))
input[defaultDictionary] = 'gettext'
manifests.push({ namespace: 'i18n', path: defaultDictionary })
if (resolvedConfig.mode === 'production') {
applyInputToOptions(input, options)
}
return options
},
getManifests () {
if (resolvedConfig.mode === 'production') return manifests
return manifests.map(manifest => ({ ...manifest, raw: posix.join(resolvedConfig.base, `/@id/${manifest.path}.js`) }))
},
generateBundle (_options, bundle) {
for (const file in bundle) {
const chunk = bundle[file]
if (chunk.type !== 'chunk' || !chunk.facadeModuleId) continue
const modules = Object.keys(chunk.modules)
if (modules.length === 0) modules.push(chunk.facadeModuleId)
for (const id of modules) {
const meta = this.getModuleInfo(id)?.meta as VitePluginOxManifestsModuleMeta
if (meta.gettext?.dictionary === true) {
(meta.manifests ??= []).push({ namespace: 'i18n', path: '' })
}
}
}
}
}
})
import path from 'path'
import fs from 'fs'
import path from 'node:path'
import fs from 'node:fs'
import fastGlob from 'fast-glob'
import { applyInputToOptions, basepath, joinUrlPaths } from '../util.js'
import type { ResolvedConfig, Plugin, ManifestChunk, Rollup } from 'vite'
async function getManifestEntryFile (basePath, extensions) {
import { definePlugin } from './plugin'
import { type Dict, applyInputToOptions, basepath, joinUrlPaths, stringifyJSON } from '../util'
import { type OxManifest, PROJECT_NAME } from '../index'
interface ExtManifestChunk extends ManifestChunk {
meta?: Rollup.CustomPluginOptions
}
async function getManifestEntryFile (basePath: string, extensions: string[]): Promise<path.ParsedPath> {
if (path.extname(basePath)) {
try {
await fs.promises.stat(basePath)
......@@ -17,24 +25,50 @@ async function getManifestEntryFile (basePath, extensions) {
try {
await fs.promises.stat(`${basePath}.${ext}`)
return path.parse(`${basePath}.${ext}`)
} catch (e) {}
} catch {}
}
throw new Error(`Cannot find file "${basePath}"`)
}
export default function ({ supportedEntryExtensions, entryPoints, manifestsAsEntryPoints } = {}) {
const manifestModules = {}
let resolvedConfig
let viteManifestPlugin
export default definePlugin(({ supportedEntryExtensions, entryPoints, manifestsAsEntryPoints }) => {
const manifestModules = new Map<string, OxManifest[]>()
let resolvedConfig: ResolvedConfig
let inputOptions: Rollup.NormalizedInputOptions
let viteManifestPlugin: Plugin
async function buildViteManifests (options: Rollup.NormalizedOutputOptions, bundle: Rollup.OutputBundle): Promise<Dict<ExtManifestChunk>> {
return new Promise((resolve, reject) => {
// local async context to satisfy type checker expecting void callback for promise constructor
(async function () {
if (typeof viteManifestPlugin.buildStart !== 'function' || typeof viteManifestPlugin.generateBundle !== 'function') {
throw new Error(`${PROJECT_NAME}: missing required callbacks in plugin 'vite:manifest'`)
}
// fake `PluginContext` to be passed to Vite's manifest plugin, needed to extract the asset data
const context = {
emitFile (file: Rollup.EmittedFile): void {
if (file.type === 'asset' && typeof file.source === 'string') {
resolve(JSON.parse(file.source) as Dict<ExtManifestChunk>)
} else {
reject(new Error(`${PROJECT_NAME}: received unexpected manifest data from plugin 'vite:manifest'`))
}
},
getFileName: () => 'Not implemented'
} as unknown as Rollup.PluginContext
// manually invoke plugin hooks (in a local async context to satisfy type checker expecting sync promise callbacks)
await viteManifestPlugin.buildStart.call(context, inputOptions)
await viteManifestPlugin.generateBundle.call(context, options, bundle, false)
})().catch(reject)
})
}
return {
name: `${PROJECT_NAME}/manifests-plugin`,
config (config) {
if (config.build) {
config.build.manifest = true
} else {
config.build = { manifest: true }
}
(config.build ??= {}).manifest = true
},
configResolved (config) {
......@@ -42,34 +76,21 @@ export default function ({ supportedEntryExtensions, entryPoints, manifestsAsEnt
if (resolvedConfig.mode !== 'production') return
// locate the vite:manifest plugin, copy and delete it
const index = config.plugins.findIndex(({ name }) => name === 'vite:manifest')
;([viteManifestPlugin] = config.plugins.splice(index, 1))
viteManifestPlugin.getManifests = (options, bundle) => {
// manually invoke plugins functions
return new Promise(resolve => {
if (viteManifestPlugin.buildStart) viteManifestPlugin.buildStart()
viteManifestPlugin.generateBundle.call({
emitFile: ({ source }) => {
resolve(JSON.parse(source))
},
getFileName: (id) => 'Not implemented'
}, options, bundle)
})
}
// extract the vite:manifest plugin from the list
const index = config.plugins.findIndex(plugin => plugin.name === 'vite:manifest')
viteManifestPlugin = (config.plugins as Plugin[]).splice(index, 1)[0]
},
async options (options) {
const input = {}
const input: Dict<string> = {}
if (entryPoints) {
const entryPointFiles = await fastGlob(entryPoints, { absolute: true })
entryPointFiles.forEach(entry => {
for (const entry of entryPointFiles) {
const baseLength = resolvedConfig.root.length + 1
const extLength = path.extname(entry).length
input[entry.substring(baseLength, entry.length - extLength)] = entry
})
}
}
// 1. load all json files
......@@ -78,9 +99,9 @@ export default function ({ supportedEntryExtensions, entryPoints, manifestsAsEnt
// 2. find every file that is referenced by that json file
for (const entry of entries) {
const rawData = await fs.promises.readFile(entry, 'utf-8')
const json = JSON.parse(rawData)
const manifests = [].concat(json)
const manifestDir = path.dirname(entry).substr(root.length + 1)
const json = JSON.parse(rawData) as OxManifest | OxManifest[]
const manifests = Array.isArray(json) ? json : [json]
const manifestDir = path.dirname(entry).substring(root.length + 1)
await Promise.all(manifests.map(async ({ path: srcManifestEntryPath = joinUrlPaths(manifestDir, 'register'), ...rest }) => {
const { dir, base } = await getManifestEntryFile(joinUrlPaths(root, srcManifestEntryPath), supportedEntryExtensions)
......@@ -91,8 +112,8 @@ export default function ({ supportedEntryExtensions, entryPoints, manifestsAsEnt
}
// 3. put the file content except path into the manifest modules
const modules = manifestModules[manifestEntryPath] || []
manifestModules[manifestEntryPath] = modules.concat({ ...rest })
const modules = manifestModules.get(manifestEntryPath)
manifestModules.set(manifestEntryPath, (modules || []).concat({ path: '', ...rest }))
}))
}
......@@ -102,35 +123,38 @@ export default function ({ supportedEntryExtensions, entryPoints, manifestsAsEnt
return options
},
buildStart (options) {
inputOptions = options
},
// specific call to collect all manifests
getManifests () {
return Object.entries(manifestModules).map(
([path, manifests]) => manifests.map(m => ({ ...m, path: basepath(path.substr(resolvedConfig.root.length + 1)) }))
return Array.from(manifestModules, ([path, manifests]) =>
manifests.map(m => ({ ...m, path: basepath(path.slice(resolvedConfig.root.length + 1)) }))
).flat(1)
},
transform (code, id) {
transform (_code, id) {
if (resolvedConfig.mode !== 'production') return
if (manifestModules[id]) {
return { meta: { manifests: manifestModules[id] } }
}
const manifests = manifestModules.get(id)
return manifests ? { meta: { manifests } } : undefined
},
async generateBundle (options, bundle) {
const viteManifests = await viteManifestPlugin.getManifests(options, bundle)
const viteManifests = await buildViteManifests(options, bundle)
for (const entry in viteManifests) {
const chunk = bundle[viteManifests[entry].file]
if (!chunk.facadeModuleId) continue
viteManifests[entry].meta = this.getModuleInfo(chunk.facadeModuleId).meta
if (chunk.type === 'chunk' && chunk.facadeModuleId) {
viteManifests[entry].meta = this.getModuleInfo(chunk.facadeModuleId)?.meta
}
}
this.emitFile({
fileName: 'manifest.json',
type: 'asset',
source: JSON.stringify(viteManifests)
source: stringifyJSON(resolvedConfig, viteManifests)
})
}
}
}
})
export default function ({ meta } = {}) {
return {
generateBundle () {
if (!meta || Object.keys(meta).length === 0) return
this.emitFile({
fileName: 'meta.json',
type: 'asset',
source: JSON.stringify(meta)
})
}
}
}
import type { ResolvedConfig } from 'vite'
import { definePlugin } from './plugin'
import { stringifyJSON } from '../util'
import { PROJECT_NAME } from '../index'
export default definePlugin(({ meta }) => {
let resolvedConfig: ResolvedConfig
return {
name: `${PROJECT_NAME}/meta-plugin`,
configResolved (config) {
resolvedConfig = config
},
generateBundle () {
if (meta && Object.keys(meta).length) {
this.emitFile({
fileName: 'meta.json',
type: 'asset',
source: stringifyJSON(resolvedConfig, meta)
})
}
}
}
})
import type { Plugin } from 'vite'
import type { GettextPluginModuleMeta } from '@open-xchange/rollup-plugin-po2json'
import type { OxManifest, VitePluginOxManifests, VitePluginOxManifestsOptions } from '../index'
/**
* Type shape of an internal manifests implementation plugin.
*/
export interface VitePluginOxManifestsPlugin extends Plugin {
pluginResolved?(plugin: VitePluginOxManifests): void
getManifests?(): OxManifest[] | Promise<OxManifest[]>
}
/**
* Type shape of internal module metadata used by the manifests plugin.
*/
export interface VitePluginOxManifestsModuleMeta extends GettextPluginModuleMeta {
manifests?: OxManifest[]
}
/**
* Plugin generator function.
*/
export type VitePluginOxManifestsPluginFn = (options: Required<VitePluginOxManifestsOptions>) => VitePluginOxManifestsPlugin;
/**
* Helper for type safety. Inspired by Vite's `defineConfig`.
*/
export function definePlugin (fn: VitePluginOxManifestsPluginFn): VitePluginOxManifestsPluginFn {
return fn
}
export default function ({ transformAbsolutePaths } = {}) {
if (transformAbsolutePaths !== undefined) console.warn("transformAbsolutePaths is no longer used. Use `base: './'` with vite 3")
return {}
}
import { definePlugin } from './plugin'
import { PROJECT_NAME } from '../index'
export default definePlugin(options => {
if ('transformAbsolutePaths' in options) console.warn("transformAbsolutePaths is no longer used. Use `base: './'` with vite 3")
return {
name: `${PROJECT_NAME}/relative-paths-plugin`
}
})
import path, { posix } from 'path'
import path, { posix } from 'node:path'
import type { ServerResponse } from 'node:http'
import parseurl from 'parseurl'
import chokidar from 'chokidar'
import type { ResolvedConfig } from 'vite'
const sendJSON = (res, data) => {
import { definePlugin } from './plugin'
import type { OxManifest, VitePluginOxManifests } from '../index'
import { PROJECT_NAME } from '../index'
function sendJSON (res: ServerResponse, data: unknown): ServerResponse {
res.statusCode = 200
res.setHeader('Content-Type', 'application/json')
res.write(JSON.stringify(data))
return res.end()
}
export default function ({ watch } = {}) {
let resolvedConfig
let manifestRegex
let depsRegex
export default definePlugin(({ watch }) => {
let resolvedConfig: ResolvedConfig
let resolvedPlugin: VitePluginOxManifests
let manifestRegex: RegExp
let depsRegex: RegExp
return {
name: `${PROJECT_NAME}/serve-plugin`,
pluginResolved (plugin) {
resolvedPlugin = plugin
},
configResolved (config) {
resolvedConfig = config
manifestRegex = new RegExp(`^${config.base}(api/manifest.json|manifests)$`)
depsRegex = new RegExp(`^${config.base}(api/deps.json|dependencies)$`)
},
async configureServer ({ ws, middlewares, moduleGraph }) {
const addTimestamp = (manifests) => {
manifests.forEach(manifest => {
configureServer ({ ws, middlewares, moduleGraph }) {
function addTimestamp (manifests: OxManifest[]): void {
for (const manifest of manifests) {
// TODO what about other file types?
const id = `${resolvedConfig.root}/${manifest.path}.js`
const moduleNode = moduleGraph.idToModuleMap.get(id)
if (!moduleNode) return
const hmrTS = moduleNode.lastHMRTimestamp
if (!hmrTS) return
manifest.raw = posix.join(resolvedConfig.base, `/${manifest.path}.js?t=${hmrTS}`)
})
const hmrTS = moduleNode?.lastHMRTimestamp
if (hmrTS) manifest.raw = posix.join(resolvedConfig.base, `/${manifest.path}.js?t=${hmrTS}`)
}
}
middlewares.use(async (req, res, next) => {
const { path } = parseurl(req)
middlewares.use((req, res, next) => {
if (req.method !== 'GET') return next()
if (manifestRegex.test(path)) {
const manifests = await this.getManifests()
await addTimestamp(manifests)
return sendJSON(res, manifests)
const url = parseurl(req)
if (!url?.path) return next()
if (manifestRegex.test(url.path)) {
resolvedPlugin.getManifests().then(manifests => {
addTimestamp(manifests)
sendJSON(res, manifests)
}, next)
return
}
if (depsRegex.test(path)) {
if (depsRegex.test(url.path)) {
return sendJSON(res, {})
}
......@@ -53,12 +68,12 @@ export default function ({ watch } = {}) {
if (watch) {
const watcher = chokidar.watch(path.join(resolvedConfig.root, '**/manifest.json'))
.on('ready', async () => {
watcher.on('all', async (path) => {
.on('ready', () => {
watcher.on('all', path => {
ws.send({ type: 'full-reload', path })
})
})
}
}
}
}
})