Skip to content

Commit

Permalink
Merge pull request #56 from jwanner83/#8-allow-instance-configuration
Browse files Browse the repository at this point in the history
#8 allow instance configuration
  • Loading branch information
jwanner83 authored May 15, 2023
2 parents a7e1e60 + e44859e commit 58898ca
Show file tree
Hide file tree
Showing 14 changed files with 364 additions and 14 deletions.
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,14 @@ the current implementation works for most use cases. but there are a few things
original implementation.

- package deduplication - _not planned \*\*\*_
- portlet configuration support - _[backlog](https://github.com/jwanner83/liferay-npm-bundler-improved/issues/8)_
- system configuration - [gathering interest](https://github.com/jwanner83/liferay-npm-bundler-improved/issues/55) \
↳ portlet instance configuration works since 1.4.0 [#8](https://github.com/jwanner83/liferay-npm-bundler-improved/issues/8)

## usage

To use the bundler, you have to install it from the `npmjs.org` registry `pnpm i --D liferay-npm-bundler-improved`
and edit your existing build command to use `liferay-npm-bundler-improved` instead of `liferay-npm-bundler`. most of the
features should work out of the box.
features will work out of the box.

### copy sources

Expand All @@ -79,11 +80,17 @@ if you have assets inside the `assets` folder which should be available through
`lnbs-copy-assets` command from your existing build command and add `--copy-assets` to the
`liferay-npm-bundler-improved` call

### configuration
portlet instance configuration will work the same way as in the official bundler. see the `examples/plain/features/configuration.json`
file for all available options. it is also possible to add translated labels, descriptions and default values with
language keys. the keys have to be defined inside the portlets own localization files.

### examples

see the examples folder for an example on how to use the `liferay-npm-bundler-improved` in a `react`, `vue` and plain `js`
portlet.

\
_\* the official bundler is
[available on github](https://github.com/liferay/liferay-frontend-projects/tree/master/projects/js-toolkit/packages/npm-bundler)_
<br>
Expand Down
2 changes: 1 addition & 1 deletion bundler/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "liferay-npm-bundler-improved",
"description": "A non-official but ultrafast drop-in replacement for the liferay-npm-bundler",
"version": "1.3.0",
"version": "1.4.0",
"author": "Jonas Wanner",
"license": "ISC",
"repository": {
Expand Down
6 changes: 6 additions & 0 deletions bundler/src/exceptions/FaultyConfigurationException.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default class FaultyConfigurationException extends Error {
constructor(message: string) {
super(message)
this.name = 'FaultyConfigurationException'
}
}
168 changes: 168 additions & 0 deletions bundler/src/handler/ConfigurationHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { promisify } from 'util'
import { access, readFile } from 'fs'
import {
PortletConfiguration,
ProcessedPortletConfiguration,
ProcessedPortletConfigurationField
} from '../types/Configuration.types'
import FaultyConfigurationException from '../exceptions/FaultyConfigurationException'
import { log } from '../log'

export default class ConfigurationHandler {
public configuration: PortletConfiguration = {}
public readonly processed: ProcessedPortletConfiguration = {
availableLanguageIds: [],
fields: []
}

private languageFiles: Map<string, string> = new Map()

async resolve(configurationPath: string, languageFiles: Map<string, string>): Promise<void> {
this.languageFiles = languageFiles

try {
await promisify(access)(configurationPath)
} catch {
throw new FaultyConfigurationException(`the configuration file does not exist in configured path "${configurationPath}" although the configuration feature is enabled. remove the configuration feature from the .npmbundlerrc file or create the configuration file.`)
}

const file = await promisify(readFile)(configurationPath)
if (!file.toString()) {
throw new FaultyConfigurationException(`the configuration file in configured path "${configurationPath}" is empty. add the base structure of the configuration file or remove the configuration feature from the .npmbundlerrc file.`)
}

try {
this.configuration = JSON.parse(file.toString()) as PortletConfiguration
} catch (e) {
throw new FaultyConfigurationException(`the configuration file in configured path "${configurationPath}" is not a valid JSON file. fix the configuration file or remove the configuration feature from the .npmbundlerrc file.`)
}

if (this.configuration.system?.name) {
log.warn(`the configuration file in configured path "${configurationPath}" contains a system configuration. the system configuration is currently not supported and will be ignored. if you need this feature, add a thumbs up to this ticket: https://github.com/jwanner83/liferay-npm-bundler-improved/issues/55`)
}

for (const file of languageFiles.keys()) {
const locale = this.getLocale(file)
if (locale) {
this.processed.availableLanguageIds.push(locale)
}
}
}

async process(languageFiles: Map<string, string>): Promise<void> {
for (const [name, value] of Object.entries(this.configuration.portletInstance.fields)) {
const type = this.getType(value.type, value.options !== undefined)
if (!type) {
throw new FaultyConfigurationException(`the portlet instance configuration "${name}" contains an unknown data type "${value.type}". the allowed types are 'float' | 'number' | 'string' | 'boolean'`)
}
// data type will never be undefined because of the check above
const dataType = this.getDataType(value.type)

const field: ProcessedPortletConfigurationField = {
name,
label: this.getTranslation(value.name, languageFiles),
dataType,
type,
tip: this.getTranslation(value.description || '', languageFiles)
}

if (value.default) {
if (type === 'select') {
field.predefinedValue = {
"": `["${value.default as string}"]`
}
} else {
if (typeof value.default === 'boolean') {
field.predefinedValue = {
"": value.default
}
} else {
field.predefinedValue = this.getTranslation(value.default, languageFiles)
}
}
}

if (value.required) {
field.required = value.required
}

if (value.options) {
field.options = []

for (const [key, option] of Object.entries(value.options)) {
field.options.push({
value: key,
label: this.getTranslation(option, languageFiles)
})
}
}

this.processed.fields.push(field)
}
}

private getTranslation(value: string, languageFiles: Map<string, string>): Record<string, string> {
const translations: Record<string, string> = {}

if (languageFiles.size === 0) {
translations[''] = value
return translations
}

for (const [file, content] of languageFiles.entries()) {
const locale = this.getLocale(file)

// extract the translation from the file
const regex = new RegExp(`^${value}=(.*)$`, 'gm')
const match = regex.exec(content)
if (match) {
translations[locale] = match[1]
} else if (locale === '') {
translations[locale] = value
}
}

return translations
}

private getLocale(value: string): string {
if (value.includes('_')) {
return value.split('_')[1].split('.')[0]
}

return ''
}

private getType(type: 'float' | 'number' | 'string' | 'boolean' | string, hasOptions?: boolean): 'numeric' | 'text' | 'checkbox' | 'select' | undefined {
switch (type) {
case 'float':
case 'number':
return 'numeric'
case 'string':
if (hasOptions) {
return 'select'
} else {
return 'text'
}
case 'boolean':
return 'checkbox'
}

return undefined
}

private getDataType(type: 'float' | 'number' | 'string' | 'boolean' | string): 'double' | 'integer' | 'string' | 'boolean' {
switch (type) {
case 'float':
return 'double'
case 'number':
return 'integer'
case 'string':
return 'string'
case 'boolean':
return 'boolean'
}

return undefined
}
}
9 changes: 9 additions & 0 deletions bundler/src/handler/FeaturesHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ export default class FeaturesHandler {
public localizationPath = `features${sep}localization`
public hasLocalization = false

public configurationPath = ''
public hasConfiguration = false

public headerCSSPath = ''
public hasHeaderCSS = false

Expand Down Expand Up @@ -38,6 +41,12 @@ export default class FeaturesHandler {
}
}

const configuration = this.npmbundlerrc?.['create-jar']?.features?.configuration
if (configuration) {
this.hasConfiguration = true
this.configurationPath = configuration.replace(/\//g, sep)
}

if (pack.portlet['com.liferay.portlet.header-portlet-css']) {
this.hasHeaderCSS = true
this.headerCSSPath = pack.portlet['com.liferay.portlet.header-portlet-css']
Expand Down
28 changes: 22 additions & 6 deletions bundler/src/handler/ProcessHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,26 @@ import JarHandler from './JarHandler'
import PackageHandler from './PackageHandler'
import SettingsHandler from './SettingsHandler'
import TemplateHandler from './TemplateHandler'
import ConfigurationHandler from './ConfigurationHandler'

export default class ProcessHandler {
private entryPoint: string
private entryPath: string

private readonly languageFiles: Map<string, string> = new Map()

private readonly settingsHandler: SettingsHandler
private readonly packageHandler: PackageHandler
private readonly jarHandler: JarHandler
private readonly featuresHandler: FeaturesHandler
private readonly configurationHandler: ConfigurationHandler

constructor(settingsHandler: SettingsHandler) {
this.settingsHandler = settingsHandler
this.packageHandler = new PackageHandler()
this.jarHandler = new JarHandler()
this.featuresHandler = new FeaturesHandler()
this.configurationHandler = new ConfigurationHandler()
}

async prepare(): Promise<void> {
Expand Down Expand Up @@ -150,12 +155,11 @@ export default class ProcessHandler {
if (this.featuresHandler.hasLocalization) {
const files = await promisify(readdir)(this.featuresHandler.localizationPath)
for (const file of files) {
this.jarHandler.archive.append(
(
await promisify(readFile)(`${this.featuresHandler.localizationPath}${sep}${file}`)
).toString(),
{ name: `/content/${file}` }
)
const content = (
await promisify(readFile)(`${this.featuresHandler.localizationPath}${sep}${file}`)
).toString()
this.languageFiles.set(file, content)
this.jarHandler.archive.append(content, { name: `/content/${file}` })
}
}

Expand Down Expand Up @@ -192,6 +196,18 @@ export default class ProcessHandler {
}
}

// process configuration
if (this.featuresHandler.hasConfiguration) {
await this.configurationHandler.resolve(this.featuresHandler.configurationPath, this.languageFiles)

if (this.configurationHandler.configuration.portletInstance?.fields !== undefined) {
await this.configurationHandler.process(this.languageFiles)
this.jarHandler.archive.append(JSON.stringify(this.configurationHandler.processed, null, 2), {
name: `/features/portlet_preferences.json`
})
}
}

// copy assets
if (this.settingsHandler.copyAssets) {
log.progress('copy assets')
Expand Down
43 changes: 43 additions & 0 deletions bundler/src/types/Configuration.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
export interface PortletConfiguration {
system?: PortletSystemConfiguration
portletInstance?: PortletInstanceConfiguration
}

export interface PortletSystemConfiguration {
name: string
}

export interface PortletInstanceConfiguration {
name: string
fields: Record<string, PortletConfigurationField>
}

export interface PortletConfigurationField {
type: 'float' | 'number' | 'string' | 'boolean' | string
name: string
description: string
default: string | boolean
required?: boolean
options?: Record<string, string>
}

export interface ProcessedPortletConfiguration {
availableLanguageIds: string[]
fields: ProcessedPortletConfigurationField[]
}

export interface ProcessedPortletConfigurationField {
name: string
label: Record<string, string>
dataType: 'double' | 'integer' | 'string' | 'boolean'
type: 'numeric' | 'text' | 'checkbox' | 'select'
tip: Record<string, string>
predefinedValue?: Record<string, string | boolean>
required?: boolean
options?: ProcessedPortletConfigurationFieldOption[]
}

export interface ProcessedPortletConfigurationFieldOption {
value: string
label: Record<string, string>
}
1 change: 1 addition & 0 deletions bundler/src/types/npmbundlerrc.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ interface createJar {

interface features {
localization?: string
configuration?: string
}

export default npmbundlerrc
3 changes: 2 additions & 1 deletion examples/plain/.npmbundlerrc
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"output-dir": "dist",
"features": {
"js-extender": true,
"web-context": "/plain"
"web-context": "/plain",
"configuration": "features/configuration.json"
}
}
}
Loading

0 comments on commit 58898ca

Please sign in to comment.