From 05fe7a8876c2e9e13fe0b1c9c13df921e26cc2a1 Mon Sep 17 00:00:00 2001 From: daniel-stoian-lgp Date: Thu, 9 Jul 2026 15:33:24 +0300 Subject: [PATCH 01/13] added vite support --- index.js | 5 +- plugins/ILibPlugin/ilib-paths.js | 49 +++++ plugins/ILibPlugin/index.js | 35 +--- plugins/PrerenderPlugin/index.js | 64 +------ plugins/PrerenderPlugin/parse-locales.js | 74 ++++++++ plugins/ViteHtmlPlugin/README.md | 34 ++++ plugins/ViteHtmlPlugin/index.js | 129 ++++++++++++++ plugins/ViteILibPlugin/README.md | 54 ++++++ plugins/ViteILibPlugin/index.js | 218 +++++++++++++++++++++++ plugins/ViteWebOSMetaPlugin/README.md | 37 ++++ plugins/ViteWebOSMetaPlugin/index.js | 126 +++++++++++++ plugins/WebOSMetaPlugin/appinfo.js | 60 +++++++ plugins/WebOSMetaPlugin/index.js | 47 +---- 13 files changed, 788 insertions(+), 144 deletions(-) create mode 100644 plugins/ILibPlugin/ilib-paths.js create mode 100644 plugins/PrerenderPlugin/parse-locales.js create mode 100644 plugins/ViteHtmlPlugin/README.md create mode 100644 plugins/ViteHtmlPlugin/index.js create mode 100644 plugins/ViteILibPlugin/README.md create mode 100644 plugins/ViteILibPlugin/index.js create mode 100644 plugins/ViteWebOSMetaPlugin/README.md create mode 100644 plugins/ViteWebOSMetaPlugin/index.js create mode 100644 plugins/WebOSMetaPlugin/appinfo.js diff --git a/index.js b/index.js index acb88a2..d1315ca 100644 --- a/index.js +++ b/index.js @@ -26,5 +26,8 @@ exportOnDemand({ PrerenderPlugin: () => require('./plugins/PrerenderPlugin'), SnapshotPlugin: () => require('./plugins/SnapshotPlugin'), VerboseLogPlugin: () => require('./plugins/VerboseLogPlugin'), - WebOSMetaPlugin: () => require('./plugins/WebOSMetaPlugin') + WebOSMetaPlugin: () => require('./plugins/WebOSMetaPlugin'), + ViteHtmlPlugin: () => require('./plugins/ViteHtmlPlugin'), + ViteILibPlugin: () => require('./plugins/ViteILibPlugin'), + ViteWebOSMetaPlugin: () => require('./plugins/ViteWebOSMetaPlugin') }); diff --git a/plugins/ILibPlugin/ilib-paths.js b/plugins/ILibPlugin/ilib-paths.js new file mode 100644 index 0000000..96bcf8d --- /dev/null +++ b/plugins/ILibPlugin/ilib-paths.js @@ -0,0 +1,49 @@ +/* eslint-env node, es6 */ +/** + * ilib-paths + * + * Path/constant helpers shared by `ILibPlugin` (webpack) and `ViteILibPlugin` + * (Vite). Kept dependency-free (only `fs`/`path`) so the Vite path can reuse them + * without pulling webpack, tapable, or fast-glob (which `ILibPlugin/index.js` + * requires at module load). + */ +const fs = require('fs'); +const path = require('path'); + +// Walk up from `dir` looking for `node_modules/`; return its path relative to cwd. +function packageSearch(dir, pkg) { + let pkgPath; + if (!path.isAbsolute(dir)) dir = path.join(process.cwd(), dir); + while (dir.length > 0 && dir !== path.dirname(dir) && !pkgPath) { + const full = path.join(dir, 'node_modules', pkg); + if (fs.existsSync(full)) { + pkgPath = path.relative(process.cwd(), full); + } else { + dir = path.dirname(dir); + } + } + return pkgPath; +} + +// Normalize a filepath to be relative to the context, using forward-slashes, and +// replace each '..' with '_', keeping in line with the file-loader and other standards. +function transformPath(context, file) { + return path + .relative(context, file) + .replace(/\\/g, '/') + .replace(/\.\.(\/)?/g, '_$1'); +} + +// The `ILIB__PATH` constant name for a bundle/package. +function bundleConst(name) { + return ( + 'ILIB_' + + path + .basename(name) + .toUpperCase() + .replace(/[-_\s]/g, '_') + + '_PATH' + ); +} + +module.exports = {packageSearch, transformPath, bundleConst}; diff --git a/plugins/ILibPlugin/index.js b/plugins/ILibPlugin/index.js index a82ffbc..1c40e8f 100644 --- a/plugins/ILibPlugin/index.js +++ b/plugins/ILibPlugin/index.js @@ -4,6 +4,7 @@ const fs = require('graceful-fs'); const {SyncWaterfallHook} = require('tapable'); const {ContextReplacementPlugin, Compilation, DefinePlugin, Template, sources} = require('webpack'); const app = require('../../option-parser'); +const {packageSearch, transformPath, bundleConst} = require('./ilib-paths'); function packageName(file) { try { @@ -13,46 +14,12 @@ function packageName(file) { } } -function packageSearch(dir, pkg) { - let pkgPath; - if (!path.isAbsolute(dir)) dir = path.join(process.cwd(), dir); - while (dir.length > 0 && dir !== path.dirname(dir) && !pkgPath) { - const full = path.join(dir, 'node_modules', pkg); - if (fs.existsSync(full)) { - pkgPath = path.relative(process.cwd(), full); - } else { - dir = path.dirname(dir); - } - } - return pkgPath; -} - // Determine if it's a NodeJS output filesystem or if it's a foreign/virtual one. // The internal webpack5 implementation of outputFileSystem is graceful-fs. function isNodeOutputFS(compiler) { return compiler.outputFileSystem && JSON.stringify(compiler.outputFileSystem) === JSON.stringify(fs); } -// Normalize a filepath to be relative to the webpack context, using forward-slashes, and -// replace each '..' with '_', keeping in line with the file-loader and other webpack standards. -function transformPath(context, file) { - return path - .relative(context, file) - .replace(/\\/g, '/') - .replace(/\.\.(\/)?/g, '_$1'); -} - -function bundleConst(name) { - return ( - 'ILIB_' + - path - .basename(name) - .toUpperCase() - .replace(/[-_\s]/g, '_') + - '_PATH' - ); -} - function resolveBundle({dir, context, symlinks, relative, publicPath}) { const bundle = {resolved: dir, path: dir, emit: true}; if (path.isAbsolute(bundle.path)) { diff --git a/plugins/PrerenderPlugin/index.js b/plugins/PrerenderPlugin/index.js index 85c6376..6bc70e8 100644 --- a/plugins/PrerenderPlugin/index.js +++ b/plugins/PrerenderPlugin/index.js @@ -6,6 +6,7 @@ const {htmlTagObjectToString} = require('html-webpack-plugin/lib/html-tags'); const {Compilation, sources} = require('webpack'); const templates = require('./templates'); const vdomServer = require('./vdom-server-render'); +const {parseLocales} = require('./parse-locales'); const prerenderPluginHooksMap = new WeakMap(); @@ -338,69 +339,6 @@ function isNodeOutputFS(compiler) { return compiler.outputFileSystem && JSON.stringify(compiler.outputFileSystem) === JSON.stringify(gracefulFs); } -// Determine the desired target locales based of option content. -// Can be a preset like 'tv' or 'signage', 'used' for all used app-level locales, 'all' for -// all locales supported by ilib, a custom json file input, or a comma-separated lists -function parseLocales(context, target) { - if (!target || target === 'none') { - return []; - } else if (Array.isArray(target)) { - return target; - } else if (target.toLowerCase() === 'webos') { - return JSON.parse(fs.readFileSync(path.join(__dirname, 'locales-webos.json'), {encoding: 'utf8'})).locales; - } else if (target === 'tv') { - return JSON.parse(fs.readFileSync(path.join(__dirname, 'locales-tv.json'), {encoding: 'utf8'})).locales; - } else if (target === 'signage') { - return JSON.parse(fs.readFileSync(path.join(__dirname, 'locales-signage.json'), {encoding: 'utf8'})).locales; - } else if (target === 'used') { - return detectLocales(path.join(context, 'resources', 'ilibmanifest.json')); - } else if (target === 'all') { - return detectLocales(path.join('node_modules', 'ilib', 'locale', 'ilibmanifest.json'), true); - } else if (/\.json$/i.test(target)) { - return JSON.parse(fs.readFileSync(target, {encoding: 'utf8'})).locales; - } else { - return target.split(/\s*[\n,]\s*/).filter(Boolean); - } -} - -// Scan an ilib manifest and detect all locales that it uses. -function detectLocales(manifest, deepestOnly) { - try { - const meta = JSON.parse(fs.readFileSync(manifest, {encoding: 'utf8'})); - const locales = []; - let curr, currLocale; - for (let i = 0; meta.files && i < meta.files.length; i++) { - curr = path.dirname(meta.files[i]); - currLocale = curr.replace(/[\\/]+/, '-'); - if (locales.indexOf(curr) === -1 && /^([a-z]{2})\b/.test(currLocale)) { - if (deepestOnly) { - // Remove any matches of parent directories. - for (let x = curr; x.indexOf('/') !== -1 || x.indexOf('\\') !== -1; x = path.dirname(x)) { - const index = locales.indexOf(x.replace(/[\\/]+/, '-')); - if (index >= 0) { - locales.splice(index, 1); - } - } - // Only add the entry if children aren't already in the list. - let childFound = false; - for (let k = 0; k < locales.length && !childFound; k++) { - childFound = locales[k].indexOf(currLocale) === 0; - } - if (!childFound) { - locales.push(currLocale); - } - } else { - locales.push(currLocale); - } - } - } - locales.sort((a, b) => a.split('-').length > b.split('-').length); - return locales; - } catch (e) { - return []; - } -} - // Simplifies and groups the locales and aliases to ensure minimal output needed. function simplifyAliases(locales, status) { const links = {}; diff --git a/plugins/PrerenderPlugin/parse-locales.js b/plugins/PrerenderPlugin/parse-locales.js new file mode 100644 index 0000000..2b01289 --- /dev/null +++ b/plugins/PrerenderPlugin/parse-locales.js @@ -0,0 +1,74 @@ +/* eslint-env node, es6 */ +/** + * parse-locales + * + * Locale-list resolution shared by `PrerenderPlugin` (webpack `--isomorphic`) and + * `ViteILibPlugin` (Vite iLib locale filtering, `-l`). + */ +const fs = require('fs'); +const path = require('path'); + +// Determine the desired target locales based of option content. +// Can be a preset like 'tv' or 'signage', 'used' for all used app-level locales, 'all' for +// all locales supported by ilib, a custom json file input, or a comma-separated lists +function parseLocales(context, target) { + if (!target || target === 'none') { + return []; + } else if (Array.isArray(target)) { + return target; + } else if (target.toLowerCase() === 'webos') { + return JSON.parse(fs.readFileSync(path.join(__dirname, 'locales-webos.json'), {encoding: 'utf8'})).locales; + } else if (target === 'tv') { + return JSON.parse(fs.readFileSync(path.join(__dirname, 'locales-tv.json'), {encoding: 'utf8'})).locales; + } else if (target === 'signage') { + return JSON.parse(fs.readFileSync(path.join(__dirname, 'locales-signage.json'), {encoding: 'utf8'})).locales; + } else if (target === 'used') { + return detectLocales(path.join(context, 'resources', 'ilibmanifest.json')); + } else if (target === 'all') { + return detectLocales(path.join('node_modules', 'ilib', 'locale', 'ilibmanifest.json'), true); + } else if (/\.json$/i.test(target)) { + return JSON.parse(fs.readFileSync(target, {encoding: 'utf8'})).locales; + } else { + return target.split(/\s*[\n,]\s*/).filter(Boolean); + } +} + +// Scan an ilib manifest and detect all locales that it uses. +function detectLocales(manifest, deepestOnly) { + try { + const meta = JSON.parse(fs.readFileSync(manifest, {encoding: 'utf8'})); + const locales = []; + let curr, currLocale; + for (let i = 0; meta.files && i < meta.files.length; i++) { + curr = path.dirname(meta.files[i]); + currLocale = curr.replace(/[\\/]+/, '-'); + if (locales.indexOf(curr) === -1 && /^([a-z]{2})\b/.test(currLocale)) { + if (deepestOnly) { + // Remove any matches of parent directories. + for (let x = curr; x.indexOf('/') !== -1 || x.indexOf('\\') !== -1; x = path.dirname(x)) { + const index = locales.indexOf(x.replace(/[\\/]+/, '-')); + if (index >= 0) { + locales.splice(index, 1); + } + } + // Only add the entry if children aren't already in the list. + let childFound = false; + for (let k = 0; k < locales.length && !childFound; k++) { + childFound = locales[k].indexOf(currLocale) === 0; + } + if (!childFound) { + locales.push(currLocale); + } + } else { + locales.push(currLocale); + } + } + } + locales.sort((a, b) => a.split('-').length > b.split('-').length); + return locales; + } catch (e) { + return []; + } +} + +module.exports = {parseLocales, detectLocales}; diff --git a/plugins/ViteHtmlPlugin/README.md b/plugins/ViteHtmlPlugin/README.md new file mode 100644 index 0000000..f297db2 --- /dev/null +++ b/plugins/ViteHtmlPlugin/README.md @@ -0,0 +1,34 @@ +# ViteHtmlPlugin + +Vite counterpart to the `HtmlWebpackPlugin` wiring used by `@enact/cli`. Enact +apps have no `index.html`; this plugin synthesizes the document from the same +`.ejs` template the webpack build uses and injects the app entry as an ES module. + +- **Dev server** — serves the synthesized document at `/`, letting Vite inject + its HMR client via `transformIndexHtml`. +- **Production build** — emits `index.html` referencing the hashed entry chunk + and linking its CSS, honoring the configured `base` public path. + +## Usage + +```js +const {ViteHtmlPlugin} = require('@enact/dev-utils'); + +module.exports = { + plugins: [ + ViteHtmlPlugin({ + entry: '/abs/path/to/combined-entry.js', + title: 'My App', + template: '/abs/path/to/html-template.ejs' + }) + ] +}; +``` + +## Options + +| Option | Type | Description | +| --- | --- | --- | +| `entry` | `string` | Absolute path to the (combined polyfills + app) entry module. | +| `title` | `string` | Document title substituted into the template's `<%= … %>` token. | +| `template` | `string` | Absolute path to an `.ejs`/`.html` template. Falls back to a built-in default when omitted. | diff --git a/plugins/ViteHtmlPlugin/index.js b/plugins/ViteHtmlPlugin/index.js new file mode 100644 index 0000000..3217660 --- /dev/null +++ b/plugins/ViteHtmlPlugin/index.js @@ -0,0 +1,129 @@ +/* eslint-env node, es6 */ +/** + * ViteHtmlPlugin + * + * Vite counterpart to the webpack `HtmlWebpackPlugin` wiring used by @enact/cli. + * Enact apps do not ship an `index.html`; Therefore, default Vite bahaviour cannot be applied + * the document is synthesized from an`.ejs` template (the same one the webpack build uses) with the app entry + * injected as an ES module. + * + * It handles both modes: + * - dev server: serves the synthesized document at `/`, letting Vite inject + * its HMR client via `transformIndexHtml`. + * - production build: emits `index.html` with the hashed entry chunk and its + * CSS linked, honoring the configured `base` public path. + * + * Options: + * entry {string} Absolute path to the (combined) app entry module. + * title {string} Document title. + * template {string} Absolute path to an .ejs/.html template (optional). + */ +const fs = require('fs'); + +// A classic (non-module) head script that runs before any deferred module script. +// Enact's `polyfills.js` and core-js reference the Node `global`, which doesn't +// exist in the browser (webpack supplied it via node-polyfill-webpack-plugin). +// Defining it on the global object makes bare `global` references to be resolved without +// modifying any code. `globalThis === window` on the browser main thread. +const NODE_GLOBALS_SHIM = ''; + +// Render the base document from the template (or a sane default), substituting +// the single `<%= ... %>` title token used by the Enact HTML template, and +// injecting the Node-globals shim into . +function baseHtml(template, title) { + let html; + if (template && fs.existsSync(template)) { + html = fs.readFileSync(template, 'utf8').replace(/<%=[^%]*%>/g, title || ''); + } else { + html = + '' + + '' + + '' + + `${title || ''}
`; + } + return /<\/head>/i.test(html) ? html.replace(/<\/head>/i, `${NODE_GLOBALS_SHIM}`) : NODE_GLOBALS_SHIM + html; +} + +// Dev: reference the source entry through Vite's filesystem endpoint so the +// combined polyfills+app module (and its CSS) is loaded and HMR-tracked. +function injectDev(html, entry) { + const src = '/@fs/' + entry.replace(/\\/g, '/'); + const script = ``; + return html.replace(/<\/body>/i, `${script}`); +} + +// Build: reference the emitted entry chunk and link its CSS. +function injectBuild(html, {scriptFile, cssFiles, base}) { + const prefix = base.endsWith('/') ? base : base + '/'; + const links = cssFiles.map(f => ``).join(''); + const script = ``; + return html.replace(/<\/head>/i, `${links}`).replace(/<\/body>/i, `${script}`); +} + +module.exports = function ViteHtmlPlugin({entry, title = '', template} = {}) { + let base = '/'; + return { + name: 'enact-vite-html', + // SPA so Vite doesn't expect an MPA-style html input. + config() { + return {appType: 'spa'}; + }, + configResolved(resolved) { + base = resolved.base || '/'; + }, + // Dev server: intercept the root request and serve the synthesized document. + configureServer(server) { + server.middlewares.use(async (req, res, next) => { + const url = req.url && req.url.split('?')[0]; + if (url !== '/' && url !== '/index.html') return next(); + try { + let html = injectDev(baseHtml(template, title), entry); + html = await server.transformIndexHtml(req.url, html); + res.statusCode = 200; + res.setHeader('Content-Type', 'text/html'); + res.end(html); + } catch (err) { + next(err); + } + }); + }, + // Production build: locate the entry chunk + its CSS and emit index.html. + generateBundle(options, bundle) { + let entryChunk; + const cssFiles = []; + const seen = new Set(); + const addCss = f => { + if (!seen.has(f)) { + seen.add(f); + cssFiles.push(f); + } + }; + + for (const fileName of Object.keys(bundle)) { + const chunk = bundle[fileName]; + if (chunk.type === 'chunk' && chunk.isEntry) { + entryChunk = chunk; + const imported = chunk.viteMetadata && chunk.viteMetadata.importedCss; + if (imported) imported.forEach(addCss); + } + } + // Include any remaining top-level CSS assets (e.g. --no-split-css output). + for (const fileName of Object.keys(bundle)) { + if (bundle[fileName].type === 'asset' && fileName.endsWith('.css')) addCss(fileName); + } + + if (!entryChunk) { + this.warn('ViteHtmlPlugin: no entry chunk found; skipping index.html emission.'); + return; + } + + const html = injectBuild(baseHtml(template, title), { + scriptFile: entryChunk.fileName, + cssFiles, + base + }); + this.emitFile({type: 'asset', fileName: 'index.html', source: html}); + } + }; +}; diff --git a/plugins/ViteILibPlugin/README.md b/plugins/ViteILibPlugin/README.md new file mode 100644 index 0000000..aa75ff6 --- /dev/null +++ b/plugins/ViteILibPlugin/README.md @@ -0,0 +1,54 @@ +# ViteILibPlugin + +Vite counterpart to the webpack `ILibPlugin`. `@enact/i18n`'s runtime loader is +bundler-agnostic (it fetches locale JSON over HTTP from a set of `ILIB_*` global +constants), so this plugin only needs to define those constants and make the data +available at the URLs they point to. + +- **Constants** (via the `config` hook → `define`): `ILIB_BASE_PATH`, + `ILIB_RESOURCES_PATH`, `ILIB_CACHE_ID`, `ILIB_NO_ASSETS`, per-app/per-theme + `ILIB__PATH`, and optional `ILIB_ADDITIONAL_RESOURCES_PATH` — matching the + values the webpack ILibPlugin computes. +- **Data** — build: copies the iLib `locale/`, app `resources/`, and theme + `resources/` trees into the output on `writeBundle` (a directory copy, not 6.7k + individual `emitFile` calls). Dev: serves them from their on-disk source via + middleware. +- **Locale filtering** (`locales` option; webpack's `enact pack -l …`) — when set, + the `ilibmanifest.json` file lists are trimmed to the requested locales plus + shared (non-locale) data, and a trimmed manifest is emitted/served in place of + the full one. Accepts a preset (`used`/`tv`/`signage`/`webos`/`all`), a `.json` + file, or a comma/newline list; without it the full trees are used. Example: + `-l en-US,ko-KR` trims ~70 MB → ~19 MB. + +The webpack plugin's neutralization of iLib's non-browser platform loaders is +handled separately in `@enact/cli`'s `vite.config.js` (`ILIB_LOADER_RE` stub). + +## Usage + +```js +const {ViteILibPlugin} = require('@enact/dev-utils'); + +module.exports = { + plugins: [ + ViteILibPlugin({ + context: appContext, // app root (defaults to cwd) + publicPath: '/', // matches Vite `base` + ilibAdditionalResourcesPath // optional + }) + ] +}; +``` + +## Options + +| Option | Default | Description | +| --- | --- | --- | +| `context` | `process.cwd()` | App root; output paths are computed relative to it. | +| `ilib` | auto-detected | iLib base package path (`@enact/i18n/ilib` or `ilib`). | +| `resources` | `'resources'` | App resources directory. | +| `publicPath` | `'/'` | Public base URL; should match Vite `base`. | +| `symlinks` | `true` | Resolve symlinked packages to their real path. | +| `emit` | `true` | Copy/serve the data. When `false`, sets `ILIB_NO_ASSETS`. | +| `cacheId` | timestamp | Value for `ILIB_CACHE_ID`. | +| `ilibAdditionalResourcesPath` | — | Extra runtime resource path. | +| `locales` | — | Locale-filter target (`-l`): preset / `.json` / comma list. When set, only the matching locale data (plus shared data) is emitted/served. | diff --git a/plugins/ViteILibPlugin/index.js b/plugins/ViteILibPlugin/index.js new file mode 100644 index 0000000..41915ca --- /dev/null +++ b/plugins/ViteILibPlugin/index.js @@ -0,0 +1,218 @@ +/* eslint-env node, es6 */ +/** + * ViteILibPlugin + * + * Vite counterpart to the webpack `ILibPlugin`. `@enact/i18n`'s runtime loader + * (`Loader.js`) is bundler-agnostic. It reads a set of `ILIB_*` global constants + * for the base URLs of iLib locale data / app `resources` / theme bundles, then + * fetches the JSON over HTTP. So this plugin only needs to: + * + * 1. `define` those `ILIB_*` constants (via the `config` hook), matching the + * values the webpack ILibPlugin computes, for the Rollup build and, via + * `optimizeDeps.esbuildOptions.define`, the dev-server pre-bundled deps. + * 2. Make the referenced data available at those URLs: + * - build → copy the source trees into the output on `writeBundle`. + * - dev → serve them from their on-disk source via middleware. + * + * Locale filtering (webpack's `enact pack -l …`) is supported via the `locales` + * option: when set, the iLib/resource `ilibmanifest.json` file lists are trimmed + * to the requested locales (plus shared, non-locale data), and a trimmed manifest + * is emitted/served in place of the full one. Without it, the full trees are + * copied/served (matching an unfiltered webpack build). + * + * The webpack plugin's neutralization of iLib's non-browser platform loaders is + * handled separately in `@enact/cli`'s `vite.config.js` (`ILIB_LOADER_RE` stub). + */ +const path = require('path'); +const fs = require('fs'); +const app = require('../../option-parser'); +// Shared with PrerenderPlugin (webpack `--isomorphic`) — dependency-free locale +// resolution, so reusing it here does not pull webpack into the Vite path. +const {parseLocales} = require('../PrerenderPlugin/parse-locales'); + +// Path/constant helpers shared with ILibPlugin (webpack) — dependency-free. +const {transformPath, bundleConst, packageSearch} = require('../ILibPlugin/ilib-paths'); + +// Join URL segments with single slashes, preserving a leading slash. +function joinUrl(...parts) { + return parts + .filter(p => p != null && p !== '') + .join('/') + .replace(/([^:])\/{2,}/g, '$1/') + .replace(/^\/{2,}/, '/'); +} + +// True if two slash-separated locale paths lie on the same root-to-leaf lineage. +function sameLineage(a, b) { + const as = a.split('/'); + const bs = b.split('/'); + const n = Math.min(as.length, bs.length); + for (let i = 0; i < n; i++) { + if (as[i] !== bs[i]) return false; + } + return true; +} + +module.exports = function ViteILibPlugin(options = {}) { + const opts = Object.assign({}, options); + const context = opts.context || process.cwd(); + const symlinks = opts.symlinks !== false; + const publicPath = opts.publicPath || '/'; + const emit = opts.emit !== false; + + // Locale filtering (webpack `-l`). Null = no filtering (emit everything). + const locales = opts.locales ? parseLocales(context, opts.locales) : null; + const allowedLocales = locales ? locales.map(l => l.replace(/-/g, '/')) : null; + + // Keep a manifest file if it's shared (non-locale) data, or its locale + // directory lies on the lineage of a requested locale. A locale directory has a + // 2-3 char lowercase language code as its first segment; the few 3-char iLib + // data dirs that would collide (nfc/nfd) and the `und` fallback are shared. + // Longer data dirs (zoneinfo, charmaps, charset, ctype, scripts, nfkc, nfkd) + // are excluded by the length test. + const SHARED_TOP = new Set(['nfc', 'nfd', 'und']); + function fileAllowed(relFile) { + if (!allowedLocales) return true; + const dir = path.dirname(relFile).replace(/\\/g, '/'); + if (dir === '.' || dir === '') return true; // root files (shared) + const first = dir.split('/')[0]; + if (SHARED_TOP.has(first) || !/^[a-z]{2,3}$/.test(first)) return true; // shared data + return allowedLocales.some(loc => sameLineage(dir, loc)); + } + + // Resolve the iLib base package (with @enact/i18n/ilib backward-compat), matching ILibPlugin. + const ilibRel = + opts.ilib || + process.env.ILIB_BASE_PATH || + packageSearch(context, path.join('@enact', 'i18n', 'ilib')) || + packageSearch(context, 'ilib'); + const resourcesRel = opts.resources || 'resources'; + + const defined = {}; + // {urlDir, srcDir, files} — files is a filtered manifest list, or null for a full copy. + const assets = []; + + function addBundle(name, dirRel, subdir) { + if (!dirRel) return; + let abs = path.isAbsolute(dirRel) ? dirRel : path.join(context, dirRel); + if (symlinks && fs.existsSync(abs)) abs = fs.realpathSync(abs); + const rel = transformPath(context, abs); + const url = joinUrl(publicPath, rel); + if (name) defined[name] = JSON.stringify(url); + const srcDir = subdir ? path.join(abs, subdir) : abs; + const urlDir = subdir ? joinUrl(rel, subdir) : rel; + if (emit && fs.existsSync(srcDir)) { + let files = null; + if (allowedLocales) { + const manifest = path.join(srcDir, 'ilibmanifest.json'); + if (fs.existsSync(manifest)) { + try { + const all = JSON.parse(fs.readFileSync(manifest, {encoding: 'utf8'})).files || []; + files = all.filter(fileAllowed); + } catch (e) { + files = null; + } + } + } + assets.push({urlDir, srcDir, files}); + } + return url; + } + + // iLib base data lives under /locale; ILIB_BASE_PATH points at . + addBundle('ILIB_BASE_PATH', ilibRel, 'locale'); + // App resources. + const resourcesUrl = addBundle('ILIB_RESOURCES_PATH', resourcesRel); + // Per-app + per-theme bundle path constants (theme resources supply locale data too). + defined[bundleConst(app.name)] = JSON.stringify(resourcesUrl); + let pkgDir = context; + for (let t = app.theme; t; t = t.theme) { + const themeDir = packageSearch(pkgDir, t.name); + if (themeDir) { + pkgDir = themeDir; + addBundle(bundleConst(t.name), themeDir, 'resources'); + } + } + + defined.ILIB_CACHE_ID = JSON.stringify(String(opts.cacheId || 'enact-ilib-' + Date.now())); + defined.ILIB_NO_ASSETS = JSON.stringify(!emit); + if (opts.ilibAdditionalResourcesPath) { + defined.ILIB_ADDITIONAL_RESOURCES_PATH = JSON.stringify(opts.ilibAdditionalResourcesPath); + } + + let resolvedBase = publicPath; + + return { + name: 'enact-vite-ilib', + // Inject the ILIB_* constants as build-time defines. `define` covers the + // Rollup build and live-transformed dev modules; `optimizeDeps.esbuildOptions + // .define` is required additionally so the constants also reach dev-server + // pre-bundled dependencies (e.g. @enact/i18n's Loader) — Vite does not apply + // `config.define` to the dep optimizer for arbitrary global constants. + config() { + return { + define: defined, + optimizeDeps: {esbuildOptions: {define: defined}} + }; + }, + configResolved(resolved) { + resolvedBase = resolved.base || publicPath; + }, + // Build: copy the data into the output — the filtered file set (with a trimmed + // manifest) when locale filtering is active, otherwise the whole tree. + writeBundle(outputOptions) { + const outDir = outputOptions.dir || (outputOptions.file && path.dirname(outputOptions.file)); + if (!outDir) return; + for (const {urlDir, srcDir, files} of assets) { + const dest = path.join(outDir, urlDir); + try { + if (files) { + fs.mkdirSync(dest, {recursive: true}); + for (const f of files) { + const s = path.join(srcDir, f); + const d = path.join(dest, f); + if (fs.existsSync(s)) { + fs.mkdirSync(path.dirname(d), {recursive: true}); + fs.copyFileSync(s, d); + } + } + fs.writeFileSync(path.join(dest, 'ilibmanifest.json'), JSON.stringify({files}, null, '\t')); + } else { + fs.cpSync(srcDir, dest, {recursive: true}); + } + } catch (e) { + this.warn(`ViteILibPlugin: failed to copy iLib data ${srcDir} -> ${dest}: ${e.message}`); + } + } + }, + // Dev: serve the data from its on-disk source; when filtering, serve the + // trimmed manifest and only allow the kept files. + configureServer(server) { + const routes = assets.map(({urlDir, srcDir, files}) => ({ + prefix: joinUrl(resolvedBase, urlDir).replace(/\/?$/, '/'), + srcDir, + allowed: files ? new Set(files.map(f => f.replace(/\\/g, '/'))) : null, + manifest: files ? JSON.stringify({files}, null, '\t') : null + })); + server.middlewares.use((req, res, next) => { + const url = decodeURIComponent((req.url || '').split('?')[0]); + const route = routes.find(r => url.startsWith(r.prefix)); + if (!route) return next(); + const rel = url.slice(route.prefix.length); + if (route.manifest && rel === 'ilibmanifest.json') { + res.statusCode = 200; + res.setHeader('Content-Type', 'application/json'); + return res.end(route.manifest); + } + if (route.allowed && !route.allowed.has(rel)) return next(); + const file = path.join(route.srcDir, rel); + if (!file.startsWith(route.srcDir) || !fs.existsSync(file) || !fs.statSync(file).isFile()) { + return next(); + } + res.statusCode = 200; + res.setHeader('Content-Type', file.endsWith('.json') ? 'application/json' : 'application/octet-stream'); + fs.createReadStream(file).pipe(res); + }); + } + }; +}; diff --git a/plugins/ViteWebOSMetaPlugin/README.md b/plugins/ViteWebOSMetaPlugin/README.md new file mode 100644 index 0000000..156394d --- /dev/null +++ b/plugins/ViteWebOSMetaPlugin/README.md @@ -0,0 +1,37 @@ +# ViteWebOSMetaPlugin + +Vite counterpart to the webpack `WebOSMetaPlugin`. Discovers the root +`appinfo.json` (project root or `./webos-meta/`) and any localized +`resources/**/appinfo.json`, and makes them — plus the image assets they +reference (`icon`, `largeIcon`, `splashBackground`, …) — available in the output. + +- **build** — writes the appinfo file(s) and copies referenced assets into the + output on `writeBundle`. +- **dev** — serves them from computed content / on-disk source via middleware. + +The `` fallback (use `appinfo.title` when no app/theme title is set) is +applied in `@enact/cli`'s `vite.config.js` via the static +`ViteWebOSMetaPlugin.readTitle(context)` helper, since `ViteHtmlPlugin` owns the +HTML document. + +## Usage + +```js +const {ViteWebOSMetaPlugin} = require('@enact/dev-utils'); + +module.exports = { + plugins: [ViteWebOSMetaPlugin({context: appContext, publicPath: '/'})] +}; +``` + +## Options + +| Option | Default | Description | +| --- | --- | --- | +| `context` | `process.cwd()` | App root. | +| `path` | — | Explicit directory to search for `appinfo.json` first. | +| `publicPath` | `'/'` | Public base URL; should match Vite `base`. | + +## Not yet migrated + +`$`-prefixed system assets (`sys-assets/<spec>/…`). diff --git a/plugins/ViteWebOSMetaPlugin/index.js b/plugins/ViteWebOSMetaPlugin/index.js new file mode 100644 index 0000000..1db182d --- /dev/null +++ b/plugins/ViteWebOSMetaPlugin/index.js @@ -0,0 +1,126 @@ +/* eslint-env node, es6 */ +/** + * ViteWebOSMetaPlugin + * + * Vite counterpart to the webpack `WebOSMetaPlugin`. Discovers the root + * `appinfo.json` (in the project root or `./webos-meta/`) plus any localized + * `resources/**\/appinfo.json`, and makes them — together with the image assets + * they reference (icons, splash, etc.) — available in the output: + * - build → written/copied into the output on `writeBundle`. + * - dev → served from computed content / on-disk source via middleware. + * + * The document `<title>` fallback (use appinfo.title when no app title is set) is + * handled in `vite.config.js` where `ViteHtmlPlugin` gets its title, since that + * plugin owns the HTML document. + * + * `$`-prefixed system assets (sys-assets/<spec>/…) are not yet handled. + * + * Options: {context, path (explicit appinfo dir), publicPath} + */ +const fs = require('fs'); +const path = require('path'); +const glob = require('fast-glob'); +// appinfo helpers shared with WebOSMetaPlugin (webpack) — dependency-free. +const {props: ASSET_PROPS, readAppInfo, rootAppInfo} = require('../WebOSMetaPlugin/appinfo'); + +const MIME = { + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.gif': 'image/gif', + '.svg': 'image/svg+xml', + '.json': 'application/json' +}; + +function joinUrl(...parts) { + return parts + .filter(p => p != null && p !== '') + .join('/') + .replace(/([^:])\/{2,}/g, '$1/') + .replace(/^\/{2,}/, '/'); +} + +// Read the appinfo title (root) for use as an HTML <title> fallback. +ViteWebOSMetaPlugin.readTitle = function (context, specific) { + const meta = rootAppInfo(context, specific); + return meta && meta.obj && meta.obj.title; +}; + +function ViteWebOSMetaPlugin(options = {}) { + const context = options.context || process.cwd(); + const scan = options.path; + const publicPath = options.publicPath || '/'; + + // Output entries: {name: output-relative path, src?: source file, source?: string content}. + const outputs = []; + + function addAssets(metaDir, outDir, appinfo) { + for (const prop of ASSET_PROPS) { + const val = appinfo[prop]; + if (val && val.charAt(0) !== '$') { + const src = path.resolve(metaDir, val); + if (fs.existsSync(src)) { + outputs.push({name: joinUrl(outDir, val), src}); + } + } + } + } + + // Root appinfo + its assets. + const meta = rootAppInfo(context, scan); + if (meta && meta.obj) { + addAssets(meta.path, '', meta.obj); + outputs.push({name: 'appinfo.json', source: JSON.stringify(meta.obj, null, '\t')}); + } + + // Localized appinfo files under resources/ + their assets. + const localized = glob.sync('resources/**/appinfo.json', {cwd: context, onlyFiles: true}); + for (const rel of localized) { + const file = path.join(context, rel); + const locMeta = readAppInfo(file); + if (locMeta) { + addAssets(path.dirname(file), path.dirname(rel), locMeta); + outputs.push({name: rel.replace(/\\/g, '/'), source: JSON.stringify(locMeta, null, '\t')}); + } + } + + let resolvedBase = publicPath; + + return { + name: 'enact-vite-webosmeta', + configResolved(resolved) { + resolvedBase = resolved.base || publicPath; + }, + // Build: write appinfo.json(s) and copy their referenced assets into the output. + writeBundle(outputOptions) { + const outDir = outputOptions.dir || (outputOptions.file && path.dirname(outputOptions.file)); + if (!outDir) return; + for (const o of outputs) { + const dest = path.join(outDir, o.name); + try { + fs.mkdirSync(path.dirname(dest), {recursive: true}); + if (o.src) fs.copyFileSync(o.src, dest); + else fs.writeFileSync(dest, o.source); + } catch (e) { + this.warn(`ViteWebOSMetaPlugin: failed to emit ${o.name}: ${e.message}`); + } + } + }, + // Dev: serve the appinfo files and assets from computed content / source. + configureServer(server) { + const routes = new Map(); + for (const o of outputs) routes.set(joinUrl(resolvedBase, o.name), o); + server.middlewares.use((req, res, next) => { + const url = decodeURIComponent((req.url || '').split('?')[0]); + const o = routes.get(url); + if (!o) return next(); + res.statusCode = 200; + res.setHeader('Content-Type', MIME[path.extname(o.name).toLowerCase()] || 'application/octet-stream'); + if (o.src) fs.createReadStream(o.src).pipe(res); + else res.end(o.source); + }); + } + }; +} + +module.exports = ViteWebOSMetaPlugin; diff --git a/plugins/WebOSMetaPlugin/appinfo.js b/plugins/WebOSMetaPlugin/appinfo.js new file mode 100644 index 0000000..641e902 --- /dev/null +++ b/plugins/WebOSMetaPlugin/appinfo.js @@ -0,0 +1,60 @@ +/* eslint-env node, es6 */ +/** + * appinfo + * + * appinfo.json helpers shared by `WebOSMetaPlugin` (webpack) and + * `ViteWebOSMetaPlugin` (Vite). Kept dependency-free (only `fs`/`path`) so the + * Vite path can reuse them without pulling webpack, tapable, or fast-glob (which + * `WebOSMetaPlugin/index.js` requires at module load). + */ +const fs = require('fs'); +const path = require('path'); + +// List of asset-pointing appinfo properties. +const props = [ + 'icon', + 'largeIcon', + 'extraLargeIcon', + 'miniicon', + 'smallicon', + 'splashicon', + 'splashBackground', + 'bgImage', + 'imageForRecents' +]; + +function readAppInfo(file) { + // Read and parse appinfo.json file if it exists. + if (fs.existsSync(file)) { + try { + const meta = JSON.parse(fs.readFileSync(file, {encoding: 'utf8'})); + return meta; + } catch (e) { + console.log('ERROR: unable to read/parse appinfo.json at ' + file); + } + } +} + +function rootAppInfo(context, specific) { + // The accepted root locations to search for the appinfo.json and its relative + // assets are project root or ./webos-meta. + const rootDir = [context, path.join(context, './webos-meta')]; + // If a specific path is requested, prepend it to the search list + if (specific) { + if (path.isAbsolute(specific)) { + rootDir.unshift(specific); + } else { + rootDir.unshift(path.join(context, specific)); + } + } + // Check each search location, and if found, return the data and path it was found at. + let meta; + for (let i = 0; i < rootDir.length; i++) { + meta = readAppInfo(path.join(rootDir[i], 'appinfo.json')); + if (meta) { + return {path: rootDir[i], obj: meta}; + } + } +} + +module.exports = {props, readAppInfo, rootAppInfo}; diff --git a/plugins/WebOSMetaPlugin/index.js b/plugins/WebOSMetaPlugin/index.js index 1ba9ead..87913e1 100644 --- a/plugins/WebOSMetaPlugin/index.js +++ b/plugins/WebOSMetaPlugin/index.js @@ -3,19 +3,8 @@ const path = require('path'); const glob = require('fast-glob'); const {SyncWaterfallHook} = require('tapable'); const {Compilation, sources} = require('webpack'); +const {props, readAppInfo, rootAppInfo} = require('./appinfo'); -// List of asset-pointing appinfo properties. -const props = [ - 'icon', - 'largeIcon', - 'extraLargeIcon', - 'miniicon', - 'smallicon', - 'splashicon', - 'splashBackground', - 'bgImage', - 'imageForRecents' -]; // System assets starting with '$' are dynamic and will be within a variable // directory within sysAssetsPath to denote system spec ('HD720', 'HD1080', etc.). let sysAssetsPath = 'sys-assets'; @@ -25,18 +14,6 @@ let variableSysPaths = null; // share assets. const assetPathCache = {}; -function readAppInfo(file) { - // Read and parse appinfo.json file if it exists. - if (fs.existsSync(file)) { - try { - const meta = JSON.parse(fs.readFileSync(file, {encoding: 'utf8'})); - return meta; - } catch (e) { - console.log('ERROR: unable to read/parse appinfo.json at ' + file); - } - } -} - function handleSysAssetPath(context, appinfo) { // If the sysAsset base is specified, override the default one if (appinfo.sysAssetsBasePath && appinfo.sysAssetsBasePath !== sysAssetsPath) { @@ -72,28 +49,6 @@ function detectSysAssets(name) { return result; } -function rootAppInfo(context, specific) { - // The accepted root locations to search for the appinfo.json and its relative - // assets are project root or ./webos-meta. - const rootDir = [context, path.join(context, './webos-meta')]; - // If a specific path is requested, prepend it to the search list - if (specific) { - if (path.isAbsolute(specific)) { - rootDir.unshift(specific); - } else { - rootDir.unshift(path.join(context, specific)); - } - } - // Check each search location, and if found, return the data and path it was found at. - let meta; - for (let i = 0; i < rootDir.length; i++) { - meta = readAppInfo(path.join(rootDir[i], 'appinfo.json')); - if (meta) { - return {path: rootDir[i], obj: meta}; - } - } -} - function addMetaAssets(metaDir, outDir, appinfo, compilation) { // For each appinfo.json property that contains a webos meta asset, resolve that asset, // and add its data to the compilation assets array. From 388b5fcf46b9c9513e20ae195fc4faabf3b83843 Mon Sep 17 00:00:00 2001 From: daniel-stoian-lgp <daniel.stoian@lgepartner.com> Date: Thu, 9 Jul 2026 17:57:59 +0300 Subject: [PATCH 02/13] added vite support --- plugins/ViteILibPlugin/index.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/plugins/ViteILibPlugin/index.js b/plugins/ViteILibPlugin/index.js index 41915ca..dcbf1e9 100644 --- a/plugins/ViteILibPlugin/index.js +++ b/plugins/ViteILibPlugin/index.js @@ -92,15 +92,19 @@ module.exports = function ViteILibPlugin(options = {}) { // {urlDir, srcDir, files} — files is a filtered manifest list, or null for a full copy. const assets = []; - function addBundle(name, dirRel, subdir) { + function addBundle(name, dirRel, subdir, loaderAppendsSubdir) { if (!dirRel) return; let abs = path.isAbsolute(dirRel) ? dirRel : path.join(context, dirRel); if (symlinks && fs.existsSync(abs)) abs = fs.realpathSync(abs); const rel = transformPath(context, abs); - const url = joinUrl(publicPath, rel); - if (name) defined[name] = JSON.stringify(url); const srcDir = subdir ? path.join(abs, subdir) : abs; const urlDir = subdir ? joinUrl(rel, subdir) : rel; + // The constant must point at the directory the runtime loader fetches from. + // For theme/app resources that is the served data dir (urlDir). For the iLib + // *base*, the loader appends the `locale/` subdir itself, so the constant + // points at the package dir (rel) instead. + const url = joinUrl(publicPath, loaderAppendsSubdir ? rel : urlDir); + if (name) defined[name] = JSON.stringify(url); if (emit && fs.existsSync(srcDir)) { let files = null; if (allowedLocales) { @@ -119,8 +123,9 @@ module.exports = function ViteILibPlugin(options = {}) { return url; } - // iLib base data lives under <ilib>/locale; ILIB_BASE_PATH points at <ilib>. - addBundle('ILIB_BASE_PATH', ilibRel, 'locale'); + // iLib base data lives under <ilib>/locale; ILIB_BASE_PATH points at <ilib> + // (the loader appends `locale/` itself). + addBundle('ILIB_BASE_PATH', ilibRel, 'locale', true); // App resources. const resourcesUrl = addBundle('ILIB_RESOURCES_PATH', resourcesRel); // Per-app + per-theme bundle path constants (theme resources supply locale data too). From df083cf2ca15312585712d456d3db9dda42b9734 Mon Sep 17 00:00:00 2001 From: daniel-stoian-lgp <daniel.stoian@lgepartner.com> Date: Fri, 10 Jul 2026 15:19:07 +0300 Subject: [PATCH 03/13] migrated pack for --no-minify, --verbose, --stats --- mixins/index.js | 6 ++ mixins/vite.js | 82 ++++++++++++++++ npm-shrinkwrap.json | 224 +++++++++++++++++++++++++++++++++++++++++++- package.json | 1 + 4 files changed, 308 insertions(+), 5 deletions(-) create mode 100644 mixins/vite.js diff --git a/mixins/index.js b/mixins/index.js index 2a5f514..acb5bef 100644 --- a/mixins/index.js +++ b/mixins/index.js @@ -26,5 +26,11 @@ module.exports = { } return config; + }, + // Vite counterpart to `apply`: shapes a resolved Vite config from the same opts + // (--no-minify, --verbose, --stats). The webpack `apply` above can't be reused + // because it mutates webpack-specific config (plugins/minimizers/output). + applyVite: function (config, opts = {}) { + return require('./vite').apply(config, opts); } }; diff --git a/mixins/vite.js b/mixins/vite.js new file mode 100644 index 0000000..42e084a --- /dev/null +++ b/mixins/vite.js @@ -0,0 +1,82 @@ +const path = require('path'); + +// Vite counterpart to the webpack `mixins.apply`. Given a resolved Vite `InlineConfig` +// and the CLI `opts`, applies the small build-shaping flags that don't belong in the +// base config factory: `--no-minify`, `--verbose`, and `--stats`. Mirrors the webpack +// mixins (unmangled.js, verbose.js, stats.js) +module.exports = { + apply: function (config, opts = {}) { + config.plugins = config.plugins || []; + + // --no-minify (private): keep Terser's optimizations/dead-code removal but + // disable mangling and beautify the output for readability. Mirrors the webpack + // `unmangled` mixin. Only meaningful when the build actually minifies (production); + // a development build is already unminified, so leave it untouched. + if (opts.minify === false && config.build && config.build.minify) { + config.build.minify = 'terser'; + config.build.terserOptions = Object.assign({}, config.build.terserOptions, { + mangle: false, + compress: true, + format: Object.assign({beautify: true, comments: true}, (config.build.terserOptions || {}).format) + }); + } + + // --verbose: raise Vite's log level and log build-phase transitions with a running + // module count. Rollup doesn't know the total module count up front, so unlike the + // webpack ProgressPlugin there's no percentage; module counts are the honest analog. + if (opts.verbose) { + config.logLevel = 'info'; + config.plugins.push(verbosePlugin()); + } + + // --stats: emit a static bundle-analysis treemap next to the build output + // (webpack: webpack-bundle-analyzer's BundleAnalyzerPlugin -> stats.html). + if (opts.stats) { + let visualizer; + try { + ({visualizer} = require('rollup-plugin-visualizer')); + } catch (e) { + console.log( + 'NOTICE: --stats requires the "rollup-plugin-visualizer" package, which is not ' + + 'installed; skipping bundle analysis.' + ); + return config; + } + const outDir = (config.build && config.build.outDir) || 'dist'; + config.plugins.push( + visualizer({ + filename: path.join(outDir, 'stats.html'), + title: 'Enact bundle analysis', + template: 'treemap', + open: false, + gzipSize: true, + brotliSize: true + }) + ); + } + + return config; + } +}; + +// Lightweight Rollup/Vite plugin that narrates the build phases when `--verbose` is set. +function verbosePlugin() { + let modules = 0; + return { + name: 'enact-vite-verbose', + apply: 'build', + buildStart() { + modules = 0; + console.log('[verbose] build start — resolving & transforming modules…'); + }, + moduleParsed() { + modules++; + }, + renderStart() { + console.log(`[verbose] render start — ${modules} modules parsed, generating chunks…`); + }, + generateBundle(options, bundle) { + console.log(`[verbose] emitting ${Object.keys(bundle).length} output file(s)…`); + } + }; +} diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 4e0c3ee..3d8e76b 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -21,6 +21,7 @@ "loader-utils": "^3.3.1", "mock-require": "^3.0.3", "resolve": "^1.22.12", + "rollup-plugin-visualizer": "^6.0.11", "tapable": "^2.3.3", "webpack-bundle-analyzer": "^5.3.0", "webpack-sources": "^3.5.0" @@ -719,7 +720,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -729,7 +729,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -1116,11 +1115,24 @@ "node": ">= 10.0" } }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -1133,7 +1145,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/commander": { @@ -1328,6 +1339,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/define-properties": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", @@ -1460,6 +1480,12 @@ "integrity": "sha512-cH1jZgJHoezfTnKfKwnScpHywTFVnJUNITDPREFdhNjiuD502+QFpG0Qk7G8jhsV/f+CEAFlIrzP1fT+IMb92g==", "license": "ISC" }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, "node_modules/enhanced-resolve": { "version": "5.24.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.0.tgz", @@ -9099,6 +9125,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-document.all": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-document.all/-/is-document.all-1.0.0.tgz", @@ -9140,6 +9181,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-generator-function": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", @@ -9369,6 +9419,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -9881,6 +9943,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/opener": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", @@ -10234,6 +10313,15 @@ "strip-ansi": "^6.0.1" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -10284,6 +10372,57 @@ "node": ">=0.10.0" } }, + "node_modules/rollup-plugin-visualizer": { + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-6.0.11.tgz", + "integrity": "sha512-TBwVHVY7buHjIKVLqr9scTVFwqZqMXINcCphPwIWKPDCOBIa+jCQfafvbjRJDZgXdq/A996Dy6yGJ/+/NtAXDQ==", + "license": "MIT", + "dependencies": { + "open": "^8.0.0", + "picomatch": "^4.0.2", + "source-map": "^0.7.4", + "yargs": "^17.5.1" + }, + "bin": { + "rollup-plugin-visualizer": "dist/bin/cli.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "rolldown": "1.x || ^1.0.0-beta", + "rollup": "2.x || 3.x || 4.x" + }, + "peerDependenciesMeta": { + "rolldown": { + "optional": true + }, + "rollup": { + "optional": true + } + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -10632,6 +10771,20 @@ "node": ">= 0.4" } }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string.prototype.trim": { "version": "1.2.11", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz", @@ -10696,7 +10849,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -11378,6 +11530,23 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/ws": { "version": "8.21.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", @@ -11399,6 +11568,51 @@ } } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 2967650..39a0fd1 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "loader-utils": "^3.3.1", "mock-require": "^3.0.3", "resolve": "^1.22.12", + "rollup-plugin-visualizer": "^6.0.11", "tapable": "^2.3.3", "webpack-bundle-analyzer": "^5.3.0", "webpack-sources": "^3.5.0" From 1da19e806d8fc97af08d9ab01caecd631fca30f4 Mon Sep 17 00:00:00 2001 From: daniel-stoian-lgp <daniel.stoian@lgepartner.com> Date: Mon, 13 Jul 2026 12:57:09 +0300 Subject: [PATCH 04/13] migrated pack for framework --- mixins/externals.js | 3 +- mixins/framework-libraries.js | 21 +++ mixins/framework.js | 9 +- mixins/vite-framework.js | 225 +++++++++++++++++++++++++++++++ plugins/PrerenderPlugin/index.js | 1 - plugins/ViteILibPlugin/index.js | 1 - 6 files changed, 251 insertions(+), 9 deletions(-) create mode 100644 mixins/framework-libraries.js create mode 100644 mixins/vite-framework.js diff --git a/mixins/externals.js b/mixins/externals.js index 08d1396..29df55c 100644 --- a/mixins/externals.js +++ b/mixins/externals.js @@ -3,6 +3,7 @@ const path = require('path'); const helper = require('../config-helper'); const packageRoot = require('../package-root'); const EnactFrameworkRefPlugin = require('../plugins/dll/EnactFrameworkRefPlugin'); +const {reactLibraries} = require('./framework-libraries'); module.exports = { apply: function (config, opts = {}) { @@ -13,7 +14,7 @@ module.exports = { const htmlPluginInstance = helper.getPluginByName(config, 'HtmlWebpackPlugin'); const webOSMetaPluginInstance = helper.getPluginByName(config, 'WebOSMetaPlugin'); - const libraries = ['@enact', 'react', 'react-dom', 'react-dom/client', 'react-dom/server', 'ilib']; + const libraries = ['@enact', ...reactLibraries, 'ilib']; const app = packageRoot(); if ( diff --git a/mixins/framework-libraries.js b/mixins/framework-libraries.js new file mode 100644 index 0000000..3c8a3af --- /dev/null +++ b/mixins/framework-libraries.js @@ -0,0 +1,21 @@ +// Shared constants for the framework/externals mixins (webpack `framework.js` + +// `externals.js` and the Vite `vite-framework.js`). The build ALGORITHMS differ per +// bundler (webpack DLL vs Vite import map), but these library lists are common. + +// @enact packages that are build/test tooling — never part of the runtime framework +// bundle (excluded from the framework glob / specifier enumeration). +const nonRuntimeEnactPackages = [ + 'dev-utils', + 'docs-utils', + 'storybook-utils', + 'ui-test-utils', + 'screenshot-test-utils' +]; + +// The react packages bundled into / externalized to the shared framework. +const reactLibraries = ['react', 'react-dom', 'react-dom/client', 'react-dom/server']; + +module.exports = { + nonRuntimeEnactPackages, + reactLibraries +}; diff --git a/mixins/framework.js b/mixins/framework.js index 4d62ebe..a76c3d4 100644 --- a/mixins/framework.js +++ b/mixins/framework.js @@ -4,6 +4,7 @@ const fastGlob = require('fast-glob'); const helper = require('../config-helper'); const packageRoot = require('../package-root'); const EnactFrameworkPlugin = require('../plugins/dll/EnactFrameworkPlugin'); +const {nonRuntimeEnactPackages, reactLibraries} = require('./framework-libraries'); module.exports = { apply: function (config, opts = {}) { @@ -24,11 +25,7 @@ module.exports = { '**/karma.conf.js', '**/build/**/*.*', '**/dist/**/*.*', - '**/@enact/dev-utils/**/*.*', - '**/@enact/docs-utils/**/*.*', - '**/@enact/storybook-utils/**/*.*', - '**/@enact/ui-test-utils/**/*.*', - '**/@enact/screenshot-test-utils/**/*.*', + ...nonRuntimeEnactPackages.map(p => '**/@enact/' + p + '/**/*.*'), '**/ilib/localedata/**/*.*', path.join(config.output.path, '*'), '**/node_modules/**/*.*', @@ -52,7 +49,7 @@ module.exports = { followSymbolicLinks: true }) ) - .concat(['react', 'react-dom', 'react-dom/client', 'react-dom/server']) + .concat(reactLibraries) }; if ( app.meta.name.startsWith('@enact/') && diff --git a/mixins/vite-framework.js b/mixins/vite-framework.js new file mode 100644 index 0000000..dcba6a4 --- /dev/null +++ b/mixins/vite-framework.js @@ -0,0 +1,225 @@ +// Vite counterpart to the webpack DLL framework/externals mixins (framework.js + +// externals.js / EnactFrameworkPlugin + EnactFrameworkRefPlugin). Instead of a webpack +// DLL registry, this builds the shared framework as reusable ESM addressed by an import +// map, and externalizes it out of app builds. See docs/vite-framework-externals-spike.md. +// +// Model (matches webpack): `--framework` builds a shared, app-independent framework from +// the FULL @enact surface + react + ilib, emitting the bundle + a manifest of every +// importable specifier -> output file. `--externals=<path>` externalizes those specifiers +// from the app build and injects an import map (+ the shared stylesheet) resolved from the +// manifest. + +const fs = require('fs'); +const path = require('path'); +const {nonRuntimeEnactPackages} = require('./framework-libraries'); + +const MANIFEST = 'enact-framework-manifest.json'; +const FRAMEWORK_CSS = 'enact.css'; + +// @enact packages that are build/test tooling, never part of the runtime framework. +const ENACT_TOOL_PKGS = new Set(nonRuntimeEnactPackages); + +// react family + ilib are CJS and need enumerate-named-export wrappers (see below). +const CJS_SPECS = ['react', 'react/jsx-runtime', 'react/jsx-dev-runtime', 'react-dom', 'react-dom/client', 'ilib']; + +// Whether an import specifier belongs in / should resolve to the shared framework. +function isFrameworkSpec(id) { + if (/^react($|\/)/.test(id) || /^react-dom($|\/)/.test(id) || id === 'ilib') return true; + const m = /^@enact\/([^/]+)/.exec(id); + return Boolean(m) && !ENACT_TOOL_PKGS.has(m[1]); +} + +function isCjsSpec(id) { + return /^react($|\/)/.test(id) || /^react-dom($|\/)/.test(id) || id === 'ilib'; +} + +// Turn a specifier into a safe, unique output basename. +function specToName(id) { + return id.replace(/[@]/g, '').replace(/[/]/g, '-').replace(/^-+/, ''); +} + +// Enumerate the importable specifiers of the shared framework, independent of any app: +// every @enact/<pkg> package root + each of its component subpaths (dirs with a +// package.json `main` or an index.js), plus the react family and ilib. +function enumerateSpecifiers(context) { + const nm = path.join(context, 'node_modules'); + const specs = new Set(CJS_SPECS); + const enactDir = path.join(nm, '@enact'); + let pkgs = []; + + try { + pkgs = fs.readdirSync(enactDir); + } catch (e) { + pkgs = []; + } + + for (const pkg of pkgs) { + if (ENACT_TOOL_PKGS.has(pkg)) continue; + const pkgDir = path.join(enactDir, pkg); + let stat; + + try { + stat = fs.statSync(pkgDir); + } catch (e) { + continue; + } + + if (!stat.isDirectory()) continue; + // package root, if importable + if (fs.existsSync(path.join(pkgDir, 'package.json')) || fs.existsSync(path.join(pkgDir, 'index.js'))) { + specs.add('@enact/' + pkg); + } + // component subpaths + let subs = []; + + try { + subs = fs.readdirSync(pkgDir); + } catch (e) { + subs = []; + } + + for (const sub of subs) { + if (sub === 'node_modules' || sub === 'tests' || sub === 'build' || sub === 'dist' || sub.startsWith('.')) { + continue; + } + const subDir = path.join(pkgDir, sub); + let sstat; + try { + sstat = fs.statSync(subDir); + } catch (e) { + continue; + } + if (!sstat.isDirectory()) continue; + if (fs.existsSync(path.join(subDir, 'package.json')) || fs.existsSync(path.join(subDir, 'index.js'))) { + specs.add('@enact/' + pkg + '/' + sub); + } + } + } + return [...specs].sort(); +} + +// Generate a re-export WRAPPER file per specifier into srcDir, returning {input, names}. +// Wrappers (never the resolved file as an entry) keep the real module transitive, which is +// what lets Vite's commonjs interop handle @enact's CJS-in-source the way a normal app +// build does. CJS specifiers additionally need explicit enumerated named exports. +function writeWrappers(specifiers, srcDir, appRequire) { + fs.mkdirSync(srcDir, {recursive: true}); + const idRe = /^[A-Za-z_$][A-Za-z0-9_$]*$/; + const input = {}; + const names = {}; + for (const spec of specifiers) { + let file; + const name = specToName(spec); + try { + if (isCjsSpec(spec)) { + const mod = appRequire(spec); + const exp = Object.keys(mod).filter(k => k !== 'default' && k !== '__esModule' && idRe.test(k)); + file = path.join(srcDir, name + '.js'); + fs.writeFileSync( + file, + `import __m from '${spec}';\nexport default __m;\n` + + (exp.length ? `export const { ${exp.join(', ')} } = __m;\n` : '') + ); + } else { + // verify it resolves before emitting a wrapper (skip phantom specifiers) + appRequire.resolve(spec); + file = path.join(srcDir, name + '.js'); + fs.writeFileSync( + file, + `import * as __m from '${spec}';\n` + + `export * from '${spec}';\n` + + `export default (__m.default !== undefined ? __m.default : __m);\n` + ); + } + } catch (e) { + continue; // unresolvable specifier; leave it out of the framework + } + input[name] = file; + names[spec] = name; + } + return {input, names}; +} + +// Mutate a resolved app-build config into the FRAMEWORK build config. +function applyFramework(config, {input, outDir}) { + config.build.outDir = outDir; + config.build.emptyOutDir = true; + // one shared stylesheet (import maps can't load per-chunk CSS) + config.build.cssCodeSplit = false; + config.build.rollupOptions = config.build.rollupOptions || {}; + config.build.rollupOptions.input = input; + config.build.rollupOptions.preserveEntrySignatures = 'strict'; + config.build.rollupOptions.output = Object.assign({}, config.build.rollupOptions.output, { + entryFileNames: '[name].js', + chunkFileNames: 'chunk-[name].js', + assetFileNames: info => ((info.name || '').endsWith('.css') ? FRAMEWORK_CSS : '[name][extname]') + }); + // The framework has no HTML document / appinfo; drop those plugins. + const DROP = new Set(['enact-vite-html', 'enact-vite-webosmeta']); + config.plugins = (config.plugins || []).flat().filter(p => !p || !DROP.has(p.name)); + return config; +} + +// Mutate a resolved app-build config to EXTERNALIZE the framework specifiers, recording +// which specifiers were actually imported into `collected`. +function applyExternals(config, collected) { + config.build.rollupOptions = config.build.rollupOptions || {}; + config.build.rollupOptions.external = id => { + if (isFrameworkSpec(id)) { + collected.add(id); + return true; + } + return false; + }; + return config; +} + +function writeManifest(outDir, names) { + const manifest = {css: fs.existsSync(path.join(outDir, FRAMEWORK_CSS)) ? FRAMEWORK_CSS : null, imports: {}}; + for (const spec of Object.keys(names)) manifest.imports[spec] = names[spec] + '.js'; + fs.writeFileSync(path.join(outDir, MANIFEST), JSON.stringify(manifest, null, 2)); + return manifest; +} + +function readManifest(frameworkPath) { + return JSON.parse(fs.readFileSync(path.join(frameworkPath, MANIFEST), {encoding: 'utf8'})); +} + +// Inject the import map + shared stylesheet <link> into the app's built index.html. +// `base` is the public URL prefix to the framework files (from --externals-public, or the +// relative ./framework path). +function injectHtml(htmlPath, manifest, collected, base) { + const imports = {}; + + for (const spec of collected) { + if (manifest.imports[spec]) imports[spec] = base.replace(/\/$/, '') + '/' + manifest.imports[spec]; + } + + let html = fs.readFileSync(htmlPath, {encoding: 'utf8'}); + const importmap = '<script type="importmap">' + JSON.stringify({imports}) + '</script>'; + // import map must precede any module import + html = html.replace(/<head[^>]*>/, m => m + '\n' + importmap); + + if (manifest.css) { + const link = '<link rel="stylesheet" href="' + base.replace(/\/$/, '') + '/' + manifest.css + '">'; + if (/<link[^>]+stylesheet/i.test(html)) { + html = html.replace(/(<link[^>]+stylesheet[^>]*>)/i, link + '\n$1'); + } else { + html = html.replace('</head>', link + '\n</head>'); + } + } + fs.writeFileSync(htmlPath, html); + return Object.keys(imports).length; +} + +// Only the functions the CLI (`pack.js`) actually calls are exported. MANIFEST / +// FRAMEWORK_CSS / isFrameworkSpec are internal helpers. +module.exports = { + enumerateSpecifiers, + writeWrappers, + applyFramework, + applyExternals, + writeManifest, + readManifest, + injectHtml +}; diff --git a/plugins/PrerenderPlugin/index.js b/plugins/PrerenderPlugin/index.js index 6bc70e8..66e5b5a 100644 --- a/plugins/PrerenderPlugin/index.js +++ b/plugins/PrerenderPlugin/index.js @@ -1,4 +1,3 @@ -const fs = require('fs'); const path = require('path'); const gracefulFs = require('graceful-fs'); const {SyncHook} = require('tapable'); diff --git a/plugins/ViteILibPlugin/index.js b/plugins/ViteILibPlugin/index.js index dcbf1e9..33d4fa0 100644 --- a/plugins/ViteILibPlugin/index.js +++ b/plugins/ViteILibPlugin/index.js @@ -29,7 +29,6 @@ const app = require('../../option-parser'); // Shared with PrerenderPlugin (webpack `--isomorphic`) — dependency-free locale // resolution, so reusing it here does not pull webpack into the Vite path. const {parseLocales} = require('../PrerenderPlugin/parse-locales'); - // Path/constant helpers shared with ILibPlugin (webpack) — dependency-free. const {transformPath, bundleConst, packageSearch} = require('../ILibPlugin/ilib-paths'); From f8655d783c42f5c589c919bce6003e8d33402308 Mon Sep 17 00:00:00 2001 From: daniel-stoian-lgp <daniel.stoian@lgepartner.com> Date: Mon, 13 Jul 2026 16:32:37 +0300 Subject: [PATCH 05/13] migrated pack for --external-polyfills --- mixins/vite-framework.js | 38 ++++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/mixins/vite-framework.js b/mixins/vite-framework.js index dcba6a4..0e41ee5 100644 --- a/mixins/vite-framework.js +++ b/mixins/vite-framework.js @@ -22,6 +22,9 @@ const ENACT_TOOL_PKGS = new Set(nonRuntimeEnactPackages); // react family + ilib are CJS and need enumerate-named-export wrappers (see below). const CJS_SPECS = ['react', 'react/jsx-runtime', 'react/jsx-dev-runtime', 'react-dom', 'react-dom/client', 'ilib']; +// The core-js polyfill entry, included in the framework only with --externals-polyfill. +const POLYFILL_SPEC = 'core-js/stable'; + // Whether an import specifier belongs in / should resolve to the shared framework. function isFrameworkSpec(id) { if (/^react($|\/)/.test(id) || /^react-dom($|\/)/.test(id) || id === 'ilib') return true; @@ -33,6 +36,11 @@ function isCjsSpec(id) { return /^react($|\/)/.test(id) || /^react-dom($|\/)/.test(id) || id === 'ilib'; } +// Side-effect-only specifiers (core-js polyfill): no default/named exports to re-export. +function isSideEffectSpec(id) { + return /^core-js(\/|$)/.test(id); +} + // Turn a specifier into a safe, unique output basename. function specToName(id) { return id.replace(/[@]/g, '').replace(/[/]/g, '-').replace(/^-+/, ''); @@ -41,9 +49,11 @@ function specToName(id) { // Enumerate the importable specifiers of the shared framework, independent of any app: // every @enact/<pkg> package root + each of its component subpaths (dirs with a // package.json `main` or an index.js), plus the react family and ilib. -function enumerateSpecifiers(context) { +function enumerateSpecifiers(context, opts = {}) { const nm = path.join(context, 'node_modules'); const specs = new Set(CJS_SPECS); + // --externals-polyfill: bundle core-js into the shared framework instead of each app. + if (opts.polyfill) specs.add(POLYFILL_SPEC); const enactDir = path.join(nm, '@enact'); let pkgs = []; @@ -108,13 +118,16 @@ function writeWrappers(specifiers, srcDir, appRequire) { const input = {}; const names = {}; for (const spec of specifiers) { - let file; const name = specToName(spec); + const file = path.join(srcDir, name + '.js'); try { - if (isCjsSpec(spec)) { + if (isSideEffectSpec(spec)) { + // core-js polyfill: side-effect only, no exports. Resolves via the build's + // core-js alias (apps don't depend on core-js directly), so no resolve check. + fs.writeFileSync(file, `import '${spec}';\n`); + } else if (isCjsSpec(spec)) { const mod = appRequire(spec); const exp = Object.keys(mod).filter(k => k !== 'default' && k !== '__esModule' && idRe.test(k)); - file = path.join(srcDir, name + '.js'); fs.writeFileSync( file, `import __m from '${spec}';\nexport default __m;\n` + @@ -123,7 +136,6 @@ function writeWrappers(specifiers, srcDir, appRequire) { } else { // verify it resolves before emitting a wrapper (skip phantom specifiers) appRequire.resolve(spec); - file = path.join(srcDir, name + '.js'); fs.writeFileSync( file, `import * as __m from '${spec}';\n` + @@ -161,11 +173,21 @@ function applyFramework(config, {input, outDir}) { } // Mutate a resolved app-build config to EXTERNALIZE the framework specifiers, recording -// which specifiers were actually imported into `collected`. -function applyExternals(config, collected) { +// which are actually imported into `collected`. Externalization is manifest-aware: a +// specifier is only externalized if the framework actually provides it, so an app whose +// imports the framework doesn't cover still builds (the uncovered pieces stay bundled). +function applyExternals(config, collected, manifest, opts = {}) { + const inManifest = id => Boolean(manifest && manifest.imports && manifest.imports[id]); + // --externals-polyfill: the framework provides core-js. Externalizing it as one unit + // needs it to stay a bare specifier, so drop the core-js alias (the app config points + // it at the CLI's copy) before it gets resolved+inlined. + if (opts.polyfill && inManifest(POLYFILL_SPEC) && config.resolve && Array.isArray(config.resolve.alias)) { + config.resolve.alias = config.resolve.alias.filter(a => String(a.find) !== 'core-js'); + } config.build.rollupOptions = config.build.rollupOptions || {}; config.build.rollupOptions.external = id => { - if (isFrameworkSpec(id)) { + const wanted = isFrameworkSpec(id) || (opts.polyfill && id === POLYFILL_SPEC); + if (wanted && inManifest(id)) { collected.add(id); return true; } From a3d42d05cb861a8d5aa93f9c920246ea3164efd9 Mon Sep 17 00:00:00 2001 From: daniel-stoian-lgp <daniel.stoian@lgepartner.com> Date: Tue, 14 Jul 2026 11:47:13 +0300 Subject: [PATCH 06/13] migrated pack for --isomorphic --- mixins/vite-isomorphic.js | 215 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 mixins/vite-isomorphic.js diff --git a/mixins/vite-isomorphic.js b/mixins/vite-isomorphic.js new file mode 100644 index 0000000..5cc1a8e --- /dev/null +++ b/mixins/vite-isomorphic.js @@ -0,0 +1,215 @@ +// Vite counterpart to the webpack `--isomorphic` prerendering (mixins/isomorphic.js + +// PrerenderPlugin). Vite has first-class SSR, so this does a real `vite build --ssr` of the +// app entry, renders it per-locale in Node (FileXHR for iLib locale data), and assembles the +// same output shape as webpack: a fallback index.html + deduped index.<variant>.html files + +// locale-map.json, plus per-locale webOS appinfo. Reuses the bundler-agnostic PrerenderPlugin +// helpers (templates.js, FileXHR). See docs/vite-isomorphic-scope.md. + +const fs = require('fs'); +const path = require('path'); +const templates = require('../plugins/PrerenderPlugin/templates'); +const FileXHR = require('../plugins/PrerenderPlugin/FileXHR'); + +// The client build ignores ALL iLib platform loaders (browser can't use them). For SSR in +// Node we KEEP the Node loader (ilib-node/NodeLoader) so iLib reads locale data from disk; +// only the Qt/Rhino/Ringo loaders are irrelevant here. +const SSR_LOADER_IGNORE = /(?:[/\\]|^\.\/lib\/)ilib-(?:qt|rhino|ringo)[\w-]*\.js$|(?:Rhino|Qt|Ringo)Loader(?:\.js)?$/; + +// Mutate a resolved client build config into the SSR build config (Node-loadable CJS bundle +// whose default export is the app element). +function applySsrBuild(config, {serverEntry, outDir}) { + config.build.outDir = outDir; + config.build.ssr = serverEntry; + config.build.rollupOptions = config.build.rollupOptions || {}; + config.build.rollupOptions.input = serverEntry; + // CJS: the bundle carries leftover require() (iLib's platform loaders); ESM throws. + config.build.rollupOptions.output = { + format: 'cjs', + entryFileNames: 'app.server.cjs', + inlineDynamicImports: true + }; + config.build.cssCodeSplit = false; + // Bundle @enact (JSX-in-.js needs the babel transform) + ilib (its dynamic imports break + // when externalized); react/react-dom stay external (Node provides them). + config.ssr = Object.assign({}, config.ssr, {noExternal: [/^@enact\//, /^ilib($|\/)/]}); + config.build.commonjsOptions = Object.assign({}, config.build.commonjsOptions, { + ignore: id => SSR_LOADER_IGNORE.test(id) + }); + // No HTML document / appinfo for the SSR bundle. + const DROP = new Set(['enact-vite-html', 'enact-vite-webosmeta']); + config.plugins = (config.plugins || []).flat().filter(p => !p || !DROP.has(p.name)); + return config; +} + +// FileXHR wrapper: Vite's base is '/', so @enact/i18n builds absolute locale URLs; FileXHR +// reads relative to cwd (the app dir), so strip the leading '/'. +function makeLocaleXHR() { + function LocaleXHR() { + this._x = new FileXHR(); + } + LocaleXHR.prototype.open = function (m, uri, async) { + this._x.open(m, String(uri).replace(/^\/+/, ''), async); + }; + LocaleXHR.prototype.addEventListener = function (e, fn) { + this._x.addEventListener(e, fn); + }; + ['response', 'responseText', 'status'].forEach(prop => + Object.defineProperty(LocaleXHR.prototype, prop, { + get() { + return this._x[prop]; + } + }) + ); + LocaleXHR.prototype.send = function () { + this._x.send(); + }; + return LocaleXHR; +} + +// Render each locale to markup, extract enact-locale-* root classes (stored separately) and +// dedupe identical (sanitized) markup. `load(locale)` returns the app element (uncached). +// `fontGenerator` (optional path) supplies per-locale font CSS, prepended as a head-append +// block (participates in dedupe; extracted into <head> during assembly). +// Returns {prerenders, attr, aliasOf}: prerenders = unique markups, attr[i] = locale classes, +// aliasOf[i] = index into prerenders for locale i. +function prerender({locales, load, renderToString, fontGenerator}) { + const LocaleXHR = makeLocaleXHR(); + let genFn = null; + if (fontGenerator) { + try { + const m = require(path.resolve(fontGenerator)); + genFn = m && (m.default || m); + } catch (e) { + genFn = null; + } + } + const prerenders = []; + const attr = []; + const aliasOf = []; + for (let i = 0; i < locales.length; i++) { + global.XMLHttpRequest = LocaleXHR; + process.env.LANG = locales[i]; + let markup = renderToString(load(locales[i])); + if (genFn) { + let style = genFn(locales[i]) || ''; + if (typeof genFn.fontOverrideGenerator === 'function') { + style += genFn.fontOverrideGenerator(locales[i]) || ''; + } + if (style) markup = '<!-- head append start -->\n' + style + '\n<!-- head append end -->' + markup; + } + attr[i] = ''; + markup = markup.replace( + /(<div[^>]*class="((?!enact-locale-)[^"])*)(\senact-locale-[^"]*)"/i, + (match, before, s, cls) => { + attr[i] = cls; + return before + '"'; + } + ); + const found = prerenders.indexOf(markup); + aliasOf[i] = found === -1 ? prerenders.push(markup) - 1 : found; + } + return {prerenders, attr, aliasOf}; +} + +// Assemble the isomorphic output from the client build's index.html + the prerenders. +// Emits the fallback index.html, the deduped index.<variant>.html file(s), and (for >1 locale) +// locale-map.json. Returns {localeMap, variantOf} where variantOf[v] is the emitted filename. +function assemble({outDir, locales, prerenders, attr, aliasOf, screenTypes = []}) { + const indexPath = path.join(outDir, 'index.html'); + const clientHtml = fs.readFileSync(indexPath, {encoding: 'utf8'}); + const rootRe = /<div id="root">\s*<\/div>/; + const jsAssets = [...clientHtml.matchAll(/<script[^>]+src="([^"]+)"[^>]*>/g)].map(m => m[1]); + + // Fallback index.html: remove the body module script, prepend the startup script that + // shows the (empty here) root then loads the JS at window.onload. + let fallback = clientHtml.replace(/<script[^>]+type="module"[^>]*><\/script>/g, ''); + // `templates.startup` appends each app asset as a CLASSIC <script> (webpack chunk model). + // Vite emits ES modules, so mark the dynamically-added script `type="module"` — a single + // `main.js` module self-loads its own chunks, so no sequential classic loading is needed. + const startupJs = templates + .startup(screenTypes, jsAssets) + .replace(/script\.type = 'text\/javascript'/, "script.type = 'module'"); + const startupTag = '<script type="text/javascript">' + startupJs + '</script>'; + fallback = fallback.replace(/<head[^>]*>/, m => m + '\n' + startupTag); + + const multi = locales.length > 1; + // Name a variant by its representative (first) locale; a single variant covering every + // locale is named 'multi' (matches webpack's grouped alias). + const variantName = v => { + if (!multi) return 'index.html'; + if (prerenders.length === 1) return 'index.multi.html'; + return 'index.' + locales.find((_, i) => aliasOf[i] === v) + '.html'; + }; + + const localeMap = {fallback: 'index.html', locales: {}}; + const variantOf = []; + for (let v = 0; v < prerenders.length; v++) { + const linked = locales.filter((_, i) => aliasOf[i] === v); + const mapping = linked.reduce( + (mm, loc) => Object.assign(mm, {[loc.toLowerCase()]: {classes: attr[locales.indexOf(loc)]}}), + {} + ); + // Split any font-CSS head-append block out of the markup: it goes into <head>, the + // remaining app markup into #root. + let appMarkup = prerenders[v]; + let headAppend = ''; + appMarkup = appMarkup.replace(/<!-- head append start -->([\s\S]*?)<!-- head append end -->/, (m, content) => { + headAppend = content.trim(); + return ''; + }); + // Only need the locale-mapping update script when a variant serves multiple locales. + const updater = templates.update(prerenders.length > 1 || linked.length > 1 ? mapping : null, null, appMarkup); + let html = fallback.replace(rootRe, '<div id="root">' + appMarkup + '</div>'); + if (headAppend) html = html.replace(/<head[^>]*>/, m => m + '\n' + headAppend); + if (updater) html = html.replace('</body>', '<script type="text/javascript">' + updater + '</script>\n</body>'); + + const fname = variantName(v); + fs.writeFileSync(path.join(outDir, fname), html); + variantOf[v] = fname; + linked.forEach(loc => { + localeMap.locales[loc] = fname; + }); + } + + // For a single locale the prerendered page IS index.html; otherwise index.html is the + // fallback shell and each locale maps to a variant file. + if (!multi) { + // variantName returned 'index.html', already written above. + } else { + fs.writeFileSync(indexPath, fallback); + fs.writeFileSync(path.join(outDir, 'locale-map.json'), JSON.stringify(localeMap, null, '\t')); + } + return {localeMap, variantOf}; +} + +// Phase D: webOS appinfo. Tag the root appinfo with usePrerendering, and (for >1 locale) +// write per-locale resources/<locale>/appinfo.json pointing `main` at the locale's variant. +function writeAppinfo({outDir, locales, localeMap}) { + const rootPath = path.join(outDir, 'appinfo.json'); + if (fs.existsSync(rootPath) && locales.length > 0) { + const root = JSON.parse(fs.readFileSync(rootPath, {encoding: 'utf8'})); + if (typeof root.usePrerendering === 'undefined') { + root.usePrerendering = true; + fs.writeFileSync(rootPath, JSON.stringify(root, null, '\t')); + } + } + if (locales.length > 1) { + for (const loc of locales) { + const variant = localeMap.locales[loc]; + if (!variant) continue; + const dir = path.join(outDir, 'resources', loc.replace(/-/g, path.sep)); + fs.mkdirSync(dir, {recursive: true}); + const p = path.join(dir, 'appinfo.json'); + let meta = {}; + try { + meta = JSON.parse(fs.readFileSync(p, {encoding: 'utf8'})); + } catch (e) { + meta = {}; + } + meta.main = variant; + fs.writeFileSync(p, JSON.stringify(meta, null, '\t')); + } + } +} + +module.exports = {applySsrBuild, prerender, assemble, writeAppinfo}; From fc5d6d01b20bc97f5c346c913de8e5f265a9b0e3 Mon Sep 17 00:00:00 2001 From: daniel-stoian-lgp <daniel.stoian@lgepartner.com> Date: Tue, 14 Jul 2026 12:04:37 +0300 Subject: [PATCH 07/13] migrated pack for --isomorphic --- plugins/ViteWebOSMetaPlugin/README.md | 12 +++++-- plugins/ViteWebOSMetaPlugin/index.js | 52 +++++++++++++++++++++++++-- 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/plugins/ViteWebOSMetaPlugin/README.md b/plugins/ViteWebOSMetaPlugin/README.md index 156394d..a45a290 100644 --- a/plugins/ViteWebOSMetaPlugin/README.md +++ b/plugins/ViteWebOSMetaPlugin/README.md @@ -32,6 +32,12 @@ module.exports = { | `path` | — | Explicit directory to search for `appinfo.json` first. | | `publicPath` | `'/'` | Public base URL; should match Vite `base`. | -## Not yet migrated - -`$`-prefixed system assets (`sys-assets/<spec>/…`). +## System assets (`$`-prefixed) + +An appinfo value starting with `$` (e.g. `"icon": "$icon.png"`) is a **system +asset**: the real file lives in a variable spec directory under `sys-assets/` +(`sys-assets/HD720/icon.png`, `sys-assets/HD1080/icon.png`, …). For each spec that +has the file, the plugin emits it preserving the `sys-assets/<spec>/` layout and +**leaves the appinfo value unchanged** — the platform resolves `$` to the active +spec at runtime. `sysAssetsBasePath` in appinfo overrides the `sys-assets` base. +Matches the webpack `WebOSMetaPlugin` behavior. diff --git a/plugins/ViteWebOSMetaPlugin/index.js b/plugins/ViteWebOSMetaPlugin/index.js index 1db182d..c49908f 100644 --- a/plugins/ViteWebOSMetaPlugin/index.js +++ b/plugins/ViteWebOSMetaPlugin/index.js @@ -13,7 +13,9 @@ * handled in `vite.config.js` where `ViteHtmlPlugin` gets its title, since that * plugin owns the HTML document. * - * `$`-prefixed system assets (sys-assets/<spec>/…) are not yet handled. + * `$`-prefixed system assets (`$icon.png` → sys-assets/<spec>/icon.png) are + * emitted preserving their `sys-assets/<spec>/` layout; the appinfo value is left + * untouched (the platform resolves `$` to the active spec at runtime). * * Options: {context, path (explicit appinfo dir), publicPath} */ @@ -53,14 +55,58 @@ function ViteWebOSMetaPlugin(options = {}) { // Output entries: {name: output-relative path, src?: source file, source?: string content}. const outputs = []; + const seenAssets = new Set(); + + function addAsset(name, src) { + // De-dupe by output name — sys-assets are shared across locales/props. + if (seenAssets.has(name)) return; + seenAssets.add(name); + outputs.push({name, src}); + } + + // System assets: appinfo values starting with '$' refer to a file within a + // variable spec directory (`$icon.png` → sys-assets/<spec>/icon.png, where + // <spec> is 'HD720', 'HD1080', etc.). `sysAssetsBasePath` overrides the base. + let sysAssetsPath = 'sys-assets'; + let variableSysPaths = null; + + function loadSysAssetDirs(appinfo) { + // Honor a per-appinfo sysAssetsBasePath override, then list the spec subdirs. + if (appinfo.sysAssetsBasePath && appinfo.sysAssetsBasePath !== sysAssetsPath) { + sysAssetsPath = appinfo.sysAssetsBasePath; + variableSysPaths = null; + } + if (!variableSysPaths) { + const base = path.join(context, sysAssetsPath); + variableSysPaths = fs.existsSync(base) + ? fs + .readdirSync(base) + .map(name => path.join(base, name)) + .filter(p => fs.statSync(p).isDirectory()) + : []; + } + } + + function detectSysAssets(val) { + // Every `<spec>/<name>` (name minus the leading '$') that exists on disk. + const trueName = val.substring(1); + return variableSysPaths.map(dir => path.resolve(dir, trueName)).filter(abs => fs.existsSync(abs)); + } function addAssets(metaDir, outDir, appinfo) { for (const prop of ASSET_PROPS) { const val = appinfo[prop]; - if (val && val.charAt(0) !== '$') { + if (!val) continue; + if (val.charAt(0) === '$') { + loadSysAssetDirs(appinfo); + for (const abs of detectSysAssets(val)) { + // Output name relative to context → preserves the sys-assets/<spec>/ path. + addAsset(path.relative(context, abs).replace(/\\/g, '/'), abs); + } + } else { const src = path.resolve(metaDir, val); if (fs.existsSync(src)) { - outputs.push({name: joinUrl(outDir, val), src}); + addAsset(joinUrl(outDir, val), src); } } } From ca7c514cc8ddf8d6ecb98876e31049870b1fbf33 Mon Sep 17 00:00:00 2001 From: daniel-stoian-lgp <daniel.stoian@lgepartner.com> Date: Tue, 14 Jul 2026 13:54:43 +0300 Subject: [PATCH 08/13] migrated pack for polyfills --- mixins/vite-isomorphic.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mixins/vite-isomorphic.js b/mixins/vite-isomorphic.js index 5cc1a8e..dce6429 100644 --- a/mixins/vite-isomorphic.js +++ b/mixins/vite-isomorphic.js @@ -36,8 +36,12 @@ function applySsrBuild(config, {serverEntry, outDir}) { ignore: id => SSR_LOADER_IGNORE.test(id) }); // No HTML document / appinfo for the SSR bundle. + // Also drop the browser Node polyfills (vite-plugin-node-polyfills:*) — the SSR + // bundle runs in Node and must use the real builtins (path/fs/crypto), not shims. const DROP = new Set(['enact-vite-html', 'enact-vite-webosmeta']); - config.plugins = (config.plugins || []).flat().filter(p => !p || !DROP.has(p.name)); + config.plugins = (config.plugins || []) + .flat() + .filter(p => !p || (!DROP.has(p.name) && !String(p.name).startsWith('vite-plugin-node-polyfills'))); return config; } From ea4ec5b8a5b46ed2b04abd1c750f6d8e653d01ae Mon Sep 17 00:00:00 2001 From: daniel-stoian-lgp <daniel.stoian@lgepartner.com> Date: Wed, 15 Jul 2026 14:38:17 +0300 Subject: [PATCH 09/13] migrated pack for --snapshot --- mixins/vite-framework.js | 75 ++++++- mixins/vite-isomorphic.js | 12 +- mixins/vite-snapshot.js | 200 ++++++++++++++++++ plugins/SnapshotPlugin/snapshot-helper-esm.js | 83 ++++++++ plugins/SnapshotPlugin/snapshot-mock.js | 29 +++ 5 files changed, 392 insertions(+), 7 deletions(-) create mode 100644 mixins/vite-snapshot.js create mode 100644 plugins/SnapshotPlugin/snapshot-helper-esm.js create mode 100644 plugins/SnapshotPlugin/snapshot-mock.js diff --git a/mixins/vite-framework.js b/mixins/vite-framework.js index 0e41ee5..26892ca 100644 --- a/mixins/vite-framework.js +++ b/mixins/vite-framework.js @@ -108,11 +108,59 @@ function enumerateSpecifiers(context, opts = {}) { return [...specs].sort(); } +// When `--framework` is built INSIDE a theme repo (e.g. limestone), that theme's own +// components live at the repo root, not in node_modules/@enact — so enumerateSpecifiers +// (which scans node_modules) misses them. Mirror webpack's `libraries.push('.')`: if the +// context package is a @enact theme (has ThemeDecorator/MoonstoneDecorator, or is +// @enact/i18n), enumerate its own component subpaths as `@enact/<name>/<sub>` specifiers. +// Returns {name, root, specs} (or null when not a theme repo). The specifiers resolve via a +// self-alias added in applyFramework (they aren't self-resolvable from the repo's node_modules). +function enumerateSelfSpecs(context) { + let pkg; + try { + pkg = JSON.parse(fs.readFileSync(path.join(context, 'package.json'), 'utf8')); + } catch (e) { + return null; + } + const name = pkg && pkg.name; + if (!name || !name.startsWith('@enact/') || ENACT_TOOL_PKGS.has(name.slice('@enact/'.length))) return null; + const isTheme = + fs.existsSync(path.join(context, 'ThemeDecorator')) || + fs.existsSync(path.join(context, 'MoonstoneDecorator')) || + name === '@enact/i18n'; + if (!isTheme) return null; + + const specs = []; + if (fs.existsSync(path.join(context, 'index.js'))) specs.push(name); + const SKIP = new Set(['node_modules', 'tests', 'build', 'dist', 'samples', 'coverage', 'resources', 'ilib']); + let subs = []; + try { + subs = fs.readdirSync(context); + } catch (e) { + subs = []; + } + for (const sub of subs) { + if (SKIP.has(sub) || sub.startsWith('.')) continue; + const subDir = path.join(context, sub); + try { + if (!fs.statSync(subDir).isDirectory()) continue; + } catch (e) { + continue; + } + if (fs.existsSync(path.join(subDir, 'package.json')) || fs.existsSync(path.join(subDir, 'index.js'))) { + specs.push(name + '/' + sub); + } + } + return specs.length ? {name, root: context, specs} : null; +} + // Generate a re-export WRAPPER file per specifier into srcDir, returning {input, names}. // Wrappers (never the resolved file as an entry) keep the real module transitive, which is // what lets Vite's commonjs interop handle @enact's CJS-in-source the way a normal app // build does. CJS specifiers additionally need explicit enumerated named exports. -function writeWrappers(specifiers, srcDir, appRequire) { +// `selfSet` (optional) marks theme-repo self-specifiers, which resolve via the self-alias +// (applyFramework) rather than appRequire — so they skip the require-resolve existence check. +function writeWrappers(specifiers, srcDir, appRequire, selfSet = null) { fs.mkdirSync(srcDir, {recursive: true}); const idRe = /^[A-Za-z_$][A-Za-z0-9_$]*$/; const input = {}; @@ -125,6 +173,16 @@ function writeWrappers(specifiers, srcDir, appRequire) { // core-js polyfill: side-effect only, no exports. Resolves via the build's // core-js alias (apps don't depend on core-js directly), so no resolve check. fs.writeFileSync(file, `import '${spec}';\n`); + } else if (selfSet && selfSet.has(spec)) { + // Theme's own component (framework built inside the theme repo): ESM source that + // resolves via the self-alias during the build; existence is already confirmed + // by enumerateSelfSpecs, so skip the appRequire resolve check. + fs.writeFileSync( + file, + `import * as __m from '${spec}';\n` + + `export * from '${spec}';\n` + + `export default (__m.default !== undefined ? __m.default : __m);\n` + ); } else if (isCjsSpec(spec)) { const mod = appRequire(spec); const exp = Object.keys(mod).filter(k => k !== 'default' && k !== '__esModule' && idRe.test(k)); @@ -153,7 +211,19 @@ function writeWrappers(specifiers, srcDir, appRequire) { } // Mutate a resolved app-build config into the FRAMEWORK build config. -function applyFramework(config, {input, outDir}) { +function applyFramework(config, {input, outDir, selfAlias}) { + // Theme-repo self-inclusion: resolve `@enact/<theme>` (root + subpaths, incl. transitive + // self-references) to the repo root, since it isn't in the repo's own node_modules. + if (selfAlias && config.resolve && Array.isArray(config.resolve.alias)) { + config.resolve.alias.unshift(selfAlias); + // The theme's own source (repo root, outside node_modules) can mix CJS (e.g. a + // `module.exports.foo = …` fontGenerator) with ESM named imports of it. Vite's default + // commonjs only covers node_modules, so extend it to the theme root so those interop. + const rootRe = new RegExp(selfAlias.replacement.replace(/\\/g, '/').replace(/[.*+?^${}()|[\]]/g, '\\$&')); + config.build.commonjsOptions = Object.assign({transformMixedEsModules: true}, config.build.commonjsOptions, { + include: [/node_modules/, rootRe] + }); + } config.build.outDir = outDir; config.build.emptyOutDir = true; // one shared stylesheet (import maps can't load per-chunk CSS) @@ -238,6 +308,7 @@ function injectHtml(htmlPath, manifest, collected, base) { // FRAMEWORK_CSS / isFrameworkSpec are internal helpers. module.exports = { enumerateSpecifiers, + enumerateSelfSpecs, writeWrappers, applyFramework, applyExternals, diff --git a/mixins/vite-isomorphic.js b/mixins/vite-isomorphic.js index dce6429..3786836 100644 --- a/mixins/vite-isomorphic.js +++ b/mixins/vite-isomorphic.js @@ -118,7 +118,7 @@ function prerender({locales, load, renderToString, fontGenerator}) { // Assemble the isomorphic output from the client build's index.html + the prerenders. // Emits the fallback index.html, the deduped index.<variant>.html file(s), and (for >1 locale) // locale-map.json. Returns {localeMap, variantOf} where variantOf[v] is the emitted filename. -function assemble({outDir, locales, prerenders, attr, aliasOf, screenTypes = []}) { +function assemble({outDir, locales, prerenders, attr, aliasOf, screenTypes = [], snapshot = false}) { const indexPath = path.join(outDir, 'index.html'); const clientHtml = fs.readFileSync(indexPath, {encoding: 'utf8'}); const rootRe = /<div id="root">\s*<\/div>/; @@ -129,10 +129,12 @@ function assemble({outDir, locales, prerenders, attr, aliasOf, screenTypes = []} let fallback = clientHtml.replace(/<script[^>]+type="module"[^>]*><\/script>/g, ''); // `templates.startup` appends each app asset as a CLASSIC <script> (webpack chunk model). // Vite emits ES modules, so mark the dynamically-added script `type="module"` — a single - // `main.js` module self-loads its own chunks, so no sequential classic loading is needed. - const startupJs = templates - .startup(screenTypes, jsAssets) - .replace(/script\.type = 'text\/javascript'/, "script.type = 'module'"); + // `main.js` module self-loads its own chunks. EXCEPT the snapshot build, whose `main.js` + // is a UMD bundle that must load as a classic script (it attaches the `App` global to + // `this`/window, which a module's undefined `this` would break); leave it classic. + const startupJs = snapshot + ? templates.startup(screenTypes, jsAssets) + : templates.startup(screenTypes, jsAssets).replace(/script\.type = 'text\/javascript'/, "script.type = 'module'"); const startupTag = '<script type="text/javascript">' + startupJs + '</script>'; fallback = fallback.replace(/<head[^>]*>/, m => m + '\n' + startupTag); diff --git a/mixins/vite-snapshot.js b/mixins/vite-snapshot.js new file mode 100644 index 0000000..4e4c5cb --- /dev/null +++ b/mixins/vite-snapshot.js @@ -0,0 +1,200 @@ +/* eslint-env node, es6 */ +/** + * vite-snapshot.js + * + * Vite/Rollup counterpart to the webpack `SnapshotPlugin` + isomorphic snapshot + * specialization. `--snapshot` implies `--isomorphic`; on top of the isomorphic + * client build it produces a **self-contained UMD** `main.js` that: + * - runs in a bare V8 (what `mksnapshot` executes) — single file, no external + * `import`/`require`, `global` shimmed, attaches the app to `global.App`; + * - prepends the (bundler-agnostic) snapshot helpers so `react-dom/client` is + * initialized against a mock window and `global.updateEnvironment` is defined; + * - substitutes `react-dom/client` → snapshot-helper (and `react-redux` → + * snapshot-redux-helper), except when imported by the helper itself. + * + * The heap that `mksnapshot` captures (after this bundle's top-level eval) becomes + * `snapshot_blob.bin`; on-device the `templates.js` startup script sees the + * preloaded `App`/`ReactDOMClient` globals and hydrates. The snapshot helpers, + * `mock-window`, and `@enact/core/snapshot` deferral are reused as-is from the + * webpack `SnapshotPlugin` — they are plain CJS with no webpack coupling. + * + * Rollup notes (why the helpers are staged under node_modules): + * - The helpers are CommonJS but live outside the app's node_modules, so Vite + * would treat them as ESM source and leave their `require()` intact. We copy + * them into `<app>/node_modules/.cache/enact-vite/snapshot/` so Vite's default + * commonjs transform (which covers node_modules) rewrites their requires. + * - `strictRequires` is scoped to those staged files so their requires stay + * function-scoped/lazy — critical because `require('react-dom/client')` must + * run *after* `mockWindow.activate()` (a hoisted top-level import would load + * react-dom against the absent real window), and `updateEnvironment()`'s + * requires must run on-device, not at snapshot time. + */ +const fs = require('fs'); +const path = require('path'); +const cp = require('child_process'); + +const SRC_DIR = path.join(__dirname, '..', 'plugins', 'SnapshotPlugin'); +const HELPER_FILES = ['snapshot-mock.js', 'snapshot-helper-esm.js', 'mock-window.js']; + +// Optional deps the snapshot helper facade references; any that don't resolve in +// the app (absent package, or a missing subpath — e.g. `fbjs` is gone in React 19, +// a theme may lack `internal/$L`) fall back to a harmless no-op so the helper's +// calls (clearResBundle, updateLocale, …) do nothing — matching "library not used". +const OPTIONAL_LIBS = ['@enact/i18n', '@enact/moonstone', '@enact/sandstone', '@enact/limestone', 'ilib', 'react-redux', 'fbjs']; + +// Prelude injected at the ABSOLUTE top of main.js (before esbuild's helper block, +// which captures `__getOwnPropDescs = Object.getOwnPropertyDescriptors` etc. up front). +// The mksnapshot V8 is OLD (chromium 53): no `globalThis` (ES2020) and no window / +// HTML head-shim, so resolve the real global object via `Function('return this')()` +// and expose it as `global`/`globalThis`; and polyfill `Object.getOwnPropertyDescriptors` +// (ES2017) that esbuild's object-spread helpers need but chrome 53 lacks (no WeakMap, so +// it stays snapshot-serializable — unlike core-js's internal state). +const SNAPSHOT_PRELUDE = + 'var global=typeof global!=="undefined"?global:' + + '(typeof globalThis!=="undefined"?globalThis:(typeof self!=="undefined"?self:Function("return this")()));' + + 'if(typeof globalThis==="undefined"){try{global.globalThis=global;}catch(e){}}' + + 'if(!Object.getOwnPropertyDescriptors){Object.getOwnPropertyDescriptors=function(o){' + + 'var r={},k=Object.getOwnPropertyNames(o),s=Object.getOwnPropertySymbols?Object.getOwnPropertySymbols(o):[],i;' + + 'for(i=0;i<k.length;i++)r[k[i]]=Object.getOwnPropertyDescriptor(o,k[i]);' + + 'for(i=0;i<s.length;i++)r[s[i]]=Object.getOwnPropertyDescriptor(o,s[i]);return r;};}\n'; + +// Prepend the prelude to the entry chunk in generateBundle (which runs AFTER the esbuild +// minify renderChunk pass), so it is genuinely the first code the snapshot V8 executes. +function snapshotPreludePlugin () { + return { + name: 'enact-snapshot-prelude', + generateBundle (options, bundle) { + for (const file of Object.keys(bundle)) { + const chunk = bundle[file]; + if (chunk.type === 'chunk' && chunk.isEntry) chunk.code = SNAPSHOT_PRELUDE + chunk.code; + } + } + }; +} + +// Copy the CJS helpers into the build cache (under node_modules so Vite transforms +// their requires) and a shared no-op module for absent optional libs. +function stageHelpers (context) { + const dir = path.join(context, 'node_modules', '.cache', 'enact-vite', 'snapshot'); + fs.mkdirSync(dir, {recursive: true}); + for (const f of HELPER_FILES) fs.copyFileSync(path.join(SRC_DIR, f), path.join(dir, f)); + const noop = path.join(dir, 'noop-module.cjs'); + fs.writeFileSync(noop, 'module.exports = new Proxy(function(){}, {get: function(){ return function(){}; }});\n'); + return { + dir, + helperEsmJs: path.join(dir, 'snapshot-helper-esm.js'), + noop + }; +} + +// Write the combined snapshot entry: the ESM helper facade first (installs the +// mock window, initializes react-dom against it, defines updateEnvironment), then +// core-js, then re-export the app's default so the UMD `App` global becomes the +// app element. +function createSnapshotEntry (dir, staged, appEntry) { + const file = path.join(dir, 'snapshot-entry.js'); + const rel = target => { + let r = path.relative(dir, target).replace(/\\/g, '/'); + if (!r.startsWith('.')) r = './' + r; + return r; + }; + // core-js polyfills are included in the snapshot by default (as in the webpack path). On a + // modern snapshot V8 (e.g. Chrome 132) the polyfills' WeakMap-based internal state + // serializes fine. Only a VERY OLD snapshot V8 (~Chrome 53) can't serialize a WeakMap + // with entries — mksnapshot throws "illegal access" → 0-byte blob — for BOTH webpack and + // Vite (verified). For such old firmware, set ENACT_SNAPSHOT_NO_COREJS=1 to drop core-js + // (the syntax stays valid via V8_SNAPSHOT_TARGET; runtime builtins go unpolyfilled). + const body = + `import ${JSON.stringify(rel(staged.helperEsmJs))};\n` + + (process.env.ENACT_SNAPSHOT_NO_COREJS ? '' : `import 'core-js/stable';\n`) + + `export {default} from ${JSON.stringify(rel(appEntry))};\n`; + fs.writeFileSync(file, body); + return file; +} + +// Rollup plugin: redirect app imports of `react-dom/client` to the ESM helper +// facade (except the facade's own import, which must reach the real module), and +// resolve absent optional libs to the shared no-op module. +function snapshotResolvePlugin (staged) { + const norm = id => id && id.replace(/\\/g, '/'); + const FACADE = norm(staged.helperEsmJs); + const inSnapshotDir = id => norm(id) && norm(id).indexOf('/enact-vite/snapshot/') !== -1; + return { + name: 'enact-snapshot-resolve', + enforce: 'pre', + async resolveId (source, importer) { + // App imports of react-dom/client → the ESM helper facade (but the facade's + // own import must reach the real module). + if (source === 'react-dom/client' && norm(importer) !== FACADE) return staged.helperEsmJs; + // The staged helper's optional Enact/i18n deps: resolve if available, else no-op. + if (inSnapshotDir(importer) && OPTIONAL_LIBS.some(l => source === l || source.startsWith(l + '/'))) { + const resolved = await this.resolve(source, importer, {skipSelf: true}); + return resolved || staged.noop; + } + return null; + } + }; +} + +// Mutate an isomorphic client config into the self-contained UMD snapshot build. +function applySnapshotBuild (config, {context, appEntry}) { + const staged = stageHelpers(context); + const snapshotEntry = createSnapshotEntry(staged.dir, staged, appEntry); + config.build = config.build || {}; + config.build.cssCodeSplit = false; + // The snapshot must parse in the target board's V8. By default the app's browserslist + // drives the output (matches the firmware for a modern webOS, e.g. Chrome 132). Only for + // a much OLDER firmware than the app targets does the bundler-generated wrapper/helper + // code need extra lowering — set V8_SNAPSHOT_TARGET (e.g. "chrome53") to force esbuild to + // lower the whole output (app code is already lowered by babel-preset-enact; this also + // catches Rollup/esbuild's helpers + the minifier output). + if (process.env.V8_SNAPSHOT_TARGET) config.build.target = process.env.V8_SNAPSHOT_TARGET; + config.build.rollupOptions = config.build.rollupOptions || {}; + config.build.rollupOptions.input = {main: snapshotEntry}; + // UMD so the app's default export is exposed as the `App` global — what mksnapshot + // captures and the startup script reads on-device (mirrors the webpack isomorphic + // `output.library='App'`/`libraryTarget='umd'`). `preserveEntrySignatures:'strict'` + // keeps that export (an app build otherwise drops entry signatures → export-less + // IIFE). `inlineDynamicImports` yields the single self-contained file mksnapshot + // runs; the banner shims `global` for the bare-V8 context. ViteHtmlPlugin is kept so + // index.html is still emitted for the isomorphic assembly. + config.build.rollupOptions.preserveEntrySignatures = 'strict'; + config.build.rollupOptions.output = Object.assign({}, config.build.rollupOptions.output, { + format: 'umd', + name: 'App', + entryFileNames: 'main.js', + inlineDynamicImports: true + }); + config.plugins = config.plugins || []; + config.plugins.unshift(snapshotResolvePlugin(staged)); + config.plugins.push(snapshotPreludePlugin()); + return config; +} + +// Spawn the V8 `mksnapshot` toolchain against the emitted bundle, producing the +// startup blob. Returns {ok, blob, error}. No-op (ok:false) when V8_MKSNAPSHOT is unset. +function runMkSnapshot ({outDir, target = 'main.js', exec = process.env.V8_MKSNAPSHOT}) { + const args = process.env.V8_SNAPSHOT_ARGS + ? process.env.V8_SNAPSHOT_ARGS.split(/\s+/) + : ['--profile-deserialization', '--random-seed=314159265', '--abort_on_uncaught_exception', '--startup-blob=snapshot_blob.bin']; + const blobArg = args.find(a => a.startsWith('--startup-blob=')); + const blob = blobArg ? blobArg.replace('--startup-blob=', '') : 'snapshot_blob.bin'; + if (!exec) return {ok: false, blob, error: new Error('V8_MKSNAPSHOT is not set')}; + const child = cp.spawnSync(exec, args.concat(target), {cwd: outDir, encoding: 'utf8'}); + if (child.status !== 0) return {ok: false, blob, error: new Error(child.stdout + '\n' + child.stderr)}; + try { + if (fs.statSync(path.join(outDir, blob)).size > 0) return {ok: true, blob}; + } catch (e) { /* fall through */ } + return {ok: false, blob, error: new Error(child.stdout + '\n' + child.stderr)}; +} + +// Record the blob in the root appinfo.json so webOS loads the snapshot at launch. +function writeSnapshotAppinfo ({outDir, blob = 'snapshot_blob.bin'}) { + const p = path.join(outDir, 'appinfo.json'); + if (!fs.existsSync(p)) return; + const meta = JSON.parse(fs.readFileSync(p, 'utf8')); + meta.v8SnapshotFile = blob; + fs.writeFileSync(p, JSON.stringify(meta, null, '\t')); +} + +module.exports = {applySnapshotBuild, runMkSnapshot, writeSnapshotAppinfo}; diff --git a/plugins/SnapshotPlugin/snapshot-helper-esm.js b/plugins/SnapshotPlugin/snapshot-helper-esm.js new file mode 100644 index 0000000..9e0ed48 --- /dev/null +++ b/plugins/SnapshotPlugin/snapshot-helper-esm.js @@ -0,0 +1,83 @@ +/* eslint-disable */ +/* + * snapshot-helper-esm.js + * + * Vite/Rollup ESM port of the CommonJS `snapshot-helper.js`. Import order is + * significant and mirrors the CJS helper's require order: + * 1. `./snapshot-mock.js` — installs the mock window (before react-dom loads) + * 2. `react-dom/client` — initializes against the mock window + * The module body then exposes the initialized client on `global.ReactDOMClient`, + * restores the real globals, and defines `global.updateEnvironment` (called + * on-device to rebind to the real window and flush the `@enact/core/snapshot` + * window-ready queue). The `react-dom/client` named exports (createRoot/hydrateRoot) + * are re-exported so an app importing them resolves through this facade. + * + * The `@enact/*`/`ilib` deps used by `updateEnvironment` are imported statically: + * they're bundled and evaluated at snapshot time (Enact is snapshot-safe by + * design), while their *methods* are only called on-device inside + * `updateEnvironment`. Packages not installed in the app resolve to a no-op (via + * the snapshot resolver), so their calls are harmless. + */ +import {ExecutionEnvironment, mockWindow, inSnapshot} from './snapshot-mock.js'; +import * as ReactDOMClientNS from 'react-dom/client'; + +import * as coreSnapshotNS from '@enact/core/snapshot'; +import * as ilibNS from 'ilib/lib/ilib'; +import * as resBundleNS from '@enact/i18n/src/resBundle'; +import * as moonstoneLNS from '@enact/moonstone/internal/$L'; +import * as sandstoneLNS from '@enact/sandstone/internal/$L'; +import * as limestoneLNS from '@enact/limestone/internal/$L'; +import * as localeNS from '@enact/i18n/locale'; + +var GLOBAL = typeof globalThis !== 'undefined' ? globalThis : global; +var ReactDOMClient = ReactDOMClientNS.default || ReactDOMClientNS; +GLOBAL.ReactDOMClient = ReactDOMClient; + +// react-dom has initialized against the mock; restore the real (absent) globals. +if (inSnapshot) mockWindow.deactivate(); + +function mod(ns) { + return (ns && ns.default) || ns || {}; +} + +GLOBAL.updateEnvironment = function () { + var canUseDOM = !!(typeof window !== 'undefined' && window.document && window.document.createElement); + ExecutionEnvironment.canUseDOM = canUseDOM; + ExecutionEnvironment.canUseWorkers = typeof Worker !== 'undefined'; + ExecutionEnvironment.canUseEventListeners = canUseDOM && !!(window.addEventListener || window.attachEvent); + ExecutionEnvironment.canUseViewport = canUseDOM && !!window.screen; + ExecutionEnvironment.isInWorker = !canUseDOM; + mockWindow.attachListeners(ExecutionEnvironment.canUseEventListeners && window); + + try { + var ilib = mod(ilibNS); + if (ilib && ilib._load) { + ilib._load._cacheValidated = false; + if (ilib.clearCache) ilib.clearCache(); + } + var resBundle = mod(resBundleNS); + if (resBundle.clearResBundle) resBundle.clearResBundle(); + [moonstoneLNS, sandstoneLNS, limestoneLNS].forEach(function (ns) { + var b = mod(ns); + if (b.clearResBundle) b.clearResBundle(); + }); + var locale = mod(localeNS); + if (locale.updateLocale) locale.updateLocale(); + } catch (e) {} + + try { + var core = mod(coreSnapshotNS); + var windowReady = core.windowReady || coreSnapshotNS.windowReady; + if (windowReady) windowReady(); + } catch (e) {} +}; + +export function createRoot () { + return ReactDOMClient.createRoot.apply(null, arguments); +} + +export function hydrateRoot () { + return ReactDOMClient.hydrateRoot.apply(null, arguments); +} + +export default ReactDOMClient; diff --git a/plugins/SnapshotPlugin/snapshot-mock.js b/plugins/SnapshotPlugin/snapshot-mock.js new file mode 100644 index 0000000..1ede4d7 --- /dev/null +++ b/plugins/SnapshotPlugin/snapshot-mock.js @@ -0,0 +1,29 @@ +/* eslint-disable */ +/* + * snapshot-mock.js + * + * Vite/Rollup-only ESM module. Activates the mock window and primes fbjs BEFORE + * `react-dom/client` is imported — `snapshot-helper-esm.js` imports this module + * first, and ES module evaluation is source-ordered, so this runs to completion + * (mock window installed) before react-dom loads. Under webpack the CJS + * `snapshot-helper.js` does the same via an in-line require order; Rollup hoists + * requires, so the ordering is expressed through ESM imports instead. + */ +import ExecutionEnvironment from 'fbjs/lib/ExecutionEnvironment'; +import mockWindow from './mock-window.js'; + +// True while building the snapshot (bare V8: no window, not Node). +export const inSnapshot = + typeof window === 'undefined' && + !(typeof global !== 'undefined' && global.process && global.process.versions && global.process.versions.node); + +if (inSnapshot) { + mockWindow.activate(); + ExecutionEnvironment.canUseDOM = true; + ExecutionEnvironment.canUseWorkers = false; + ExecutionEnvironment.canUseEventListeners = true; + ExecutionEnvironment.canUseViewport = true; + ExecutionEnvironment.isInWorker = false; +} + +export {ExecutionEnvironment, mockWindow}; From 66293ecaa28b31d7db2772bf4b3d7888ea5b3d5b Mon Sep 17 00:00:00 2001 From: daniel-stoian-lgp <daniel.stoian@lgepartner.com> Date: Wed, 15 Jul 2026 16:13:56 +0300 Subject: [PATCH 10/13] migrated pack for --snapshot --- mixins/vite-isomorphic.js | 4 +++- mixins/vite-snapshot.js | 39 +++++++++++++++++++++++++++------------ 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/mixins/vite-isomorphic.js b/mixins/vite-isomorphic.js index 3786836..a9a0d92 100644 --- a/mixins/vite-isomorphic.js +++ b/mixins/vite-isomorphic.js @@ -134,7 +134,9 @@ function assemble({outDir, locales, prerenders, attr, aliasOf, screenTypes = [], // `this`/window, which a module's undefined `this` would break); leave it classic. const startupJs = snapshot ? templates.startup(screenTypes, jsAssets) - : templates.startup(screenTypes, jsAssets).replace(/script\.type = 'text\/javascript'/, "script.type = 'module'"); + : templates + .startup(screenTypes, jsAssets) + .replace(/script\.type = 'text\/javascript'/, "script.type = 'module'"); const startupTag = '<script type="text/javascript">' + startupJs + '</script>'; fallback = fallback.replace(/<head[^>]*>/, m => m + '\n' + startupTag); diff --git a/mixins/vite-snapshot.js b/mixins/vite-snapshot.js index 4e4c5cb..439acc5 100644 --- a/mixins/vite-snapshot.js +++ b/mixins/vite-snapshot.js @@ -40,7 +40,15 @@ const HELPER_FILES = ['snapshot-mock.js', 'snapshot-helper-esm.js', 'mock-window // the app (absent package, or a missing subpath — e.g. `fbjs` is gone in React 19, // a theme may lack `internal/$L`) fall back to a harmless no-op so the helper's // calls (clearResBundle, updateLocale, …) do nothing — matching "library not used". -const OPTIONAL_LIBS = ['@enact/i18n', '@enact/moonstone', '@enact/sandstone', '@enact/limestone', 'ilib', 'react-redux', 'fbjs']; +const OPTIONAL_LIBS = [ + '@enact/i18n', + '@enact/moonstone', + '@enact/sandstone', + '@enact/limestone', + 'ilib', + 'react-redux', + 'fbjs' +]; // Prelude injected at the ABSOLUTE top of main.js (before esbuild's helper block, // which captures `__getOwnPropDescs = Object.getOwnPropertyDescriptors` etc. up front). @@ -60,10 +68,10 @@ const SNAPSHOT_PRELUDE = // Prepend the prelude to the entry chunk in generateBundle (which runs AFTER the esbuild // minify renderChunk pass), so it is genuinely the first code the snapshot V8 executes. -function snapshotPreludePlugin () { +function snapshotPreludePlugin() { return { name: 'enact-snapshot-prelude', - generateBundle (options, bundle) { + generateBundle(options, bundle) { for (const file of Object.keys(bundle)) { const chunk = bundle[file]; if (chunk.type === 'chunk' && chunk.isEntry) chunk.code = SNAPSHOT_PRELUDE + chunk.code; @@ -74,7 +82,7 @@ function snapshotPreludePlugin () { // Copy the CJS helpers into the build cache (under node_modules so Vite transforms // their requires) and a shared no-op module for absent optional libs. -function stageHelpers (context) { +function stageHelpers(context) { const dir = path.join(context, 'node_modules', '.cache', 'enact-vite', 'snapshot'); fs.mkdirSync(dir, {recursive: true}); for (const f of HELPER_FILES) fs.copyFileSync(path.join(SRC_DIR, f), path.join(dir, f)); @@ -91,7 +99,7 @@ function stageHelpers (context) { // mock window, initializes react-dom against it, defines updateEnvironment), then // core-js, then re-export the app's default so the UMD `App` global becomes the // app element. -function createSnapshotEntry (dir, staged, appEntry) { +function createSnapshotEntry(dir, staged, appEntry) { const file = path.join(dir, 'snapshot-entry.js'); const rel = target => { let r = path.relative(dir, target).replace(/\\/g, '/'); @@ -115,14 +123,14 @@ function createSnapshotEntry (dir, staged, appEntry) { // Rollup plugin: redirect app imports of `react-dom/client` to the ESM helper // facade (except the facade's own import, which must reach the real module), and // resolve absent optional libs to the shared no-op module. -function snapshotResolvePlugin (staged) { +function snapshotResolvePlugin(staged) { const norm = id => id && id.replace(/\\/g, '/'); const FACADE = norm(staged.helperEsmJs); const inSnapshotDir = id => norm(id) && norm(id).indexOf('/enact-vite/snapshot/') !== -1; return { name: 'enact-snapshot-resolve', enforce: 'pre', - async resolveId (source, importer) { + async resolveId(source, importer) { // App imports of react-dom/client → the ESM helper facade (but the facade's // own import must reach the real module). if (source === 'react-dom/client' && norm(importer) !== FACADE) return staged.helperEsmJs; @@ -137,7 +145,7 @@ function snapshotResolvePlugin (staged) { } // Mutate an isomorphic client config into the self-contained UMD snapshot build. -function applySnapshotBuild (config, {context, appEntry}) { +function applySnapshotBuild(config, {context, appEntry}) { const staged = stageHelpers(context); const snapshotEntry = createSnapshotEntry(staged.dir, staged, appEntry); config.build = config.build || {}; @@ -173,10 +181,15 @@ function applySnapshotBuild (config, {context, appEntry}) { // Spawn the V8 `mksnapshot` toolchain against the emitted bundle, producing the // startup blob. Returns {ok, blob, error}. No-op (ok:false) when V8_MKSNAPSHOT is unset. -function runMkSnapshot ({outDir, target = 'main.js', exec = process.env.V8_MKSNAPSHOT}) { +function runMkSnapshot({outDir, target = 'main.js', exec = process.env.V8_MKSNAPSHOT}) { const args = process.env.V8_SNAPSHOT_ARGS ? process.env.V8_SNAPSHOT_ARGS.split(/\s+/) - : ['--profile-deserialization', '--random-seed=314159265', '--abort_on_uncaught_exception', '--startup-blob=snapshot_blob.bin']; + : [ + '--profile-deserialization', + '--random-seed=314159265', + '--abort_on_uncaught_exception', + '--startup-blob=snapshot_blob.bin' + ]; const blobArg = args.find(a => a.startsWith('--startup-blob=')); const blob = blobArg ? blobArg.replace('--startup-blob=', '') : 'snapshot_blob.bin'; if (!exec) return {ok: false, blob, error: new Error('V8_MKSNAPSHOT is not set')}; @@ -184,12 +197,14 @@ function runMkSnapshot ({outDir, target = 'main.js', exec = process.env.V8_MKSNA if (child.status !== 0) return {ok: false, blob, error: new Error(child.stdout + '\n' + child.stderr)}; try { if (fs.statSync(path.join(outDir, blob)).size > 0) return {ok: true, blob}; - } catch (e) { /* fall through */ } + } catch (e) { + /* fall through */ + } return {ok: false, blob, error: new Error(child.stdout + '\n' + child.stderr)}; } // Record the blob in the root appinfo.json so webOS loads the snapshot at launch. -function writeSnapshotAppinfo ({outDir, blob = 'snapshot_blob.bin'}) { +function writeSnapshotAppinfo({outDir, blob = 'snapshot_blob.bin'}) { const p = path.join(outDir, 'appinfo.json'); if (!fs.existsSync(p)) return; const meta = JSON.parse(fs.readFileSync(p, 'utf8')); From 15889b4c89c5d2540faf4bee6cd033ba98298c46 Mon Sep 17 00:00:00 2001 From: daniel-stoian-lgp <daniel.stoian@lgepartner.com> Date: Wed, 15 Jul 2026 17:14:03 +0300 Subject: [PATCH 11/13] migrated pack for --snapshot --- mixins/vite-isomorphic.js | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/mixins/vite-isomorphic.js b/mixins/vite-isomorphic.js index a9a0d92..0d0c5b6 100644 --- a/mixins/vite-isomorphic.js +++ b/mixins/vite-isomorphic.js @@ -45,27 +45,21 @@ function applySsrBuild(config, {serverEntry, outDir}) { return config; } -// FileXHR wrapper: Vite's base is '/', so @enact/i18n builds absolute locale URLs; FileXHR +// FileXHR subclass: Vite's base is '/', so @enact/i18n builds absolute locale URLs; FileXHR // reads relative to cwd (the app dir), so strip the leading '/'. +// Must SUBCLASS rather than wrap: the `xhr` package that @enact/i18n's loader uses assigns +// its completion handler as a property (`xhr.onload = fn`) instead of calling +// addEventListener, and FileXHR.send() invokes `this.onload`. A wrapper holding a private +// FileXHR would strand that handler on the outer object, so no locale data would ever load +// and every locale would prerender unlocalized. function makeLocaleXHR() { function LocaleXHR() { - this._x = new FileXHR(); + FileXHR.call(this); } + LocaleXHR.prototype = Object.create(FileXHR.prototype); + LocaleXHR.prototype.constructor = LocaleXHR; LocaleXHR.prototype.open = function (m, uri, async) { - this._x.open(m, String(uri).replace(/^\/+/, ''), async); - }; - LocaleXHR.prototype.addEventListener = function (e, fn) { - this._x.addEventListener(e, fn); - }; - ['response', 'responseText', 'status'].forEach(prop => - Object.defineProperty(LocaleXHR.prototype, prop, { - get() { - return this._x[prop]; - } - }) - ); - LocaleXHR.prototype.send = function () { - this._x.send(); + FileXHR.prototype.open.call(this, m, String(uri).replace(/^\/+/, ''), async); }; return LocaleXHR; } From 19ca8790a29c1c4e1cd767cadb34b16391e699cb Mon Sep 17 00:00:00 2001 From: daniel-stoian-lgp <daniel.stoian@lgepartner.com> Date: Fri, 17 Jul 2026 12:33:02 +0300 Subject: [PATCH 12/13] added changelog --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb4e637..ce17b82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# unreleased + +* `ViteHtmlPlugin`: Added a new plugin to render the HTML document for Vite builds, the counterpart to `HtmlWebpackPlugin`. +* `ViteILibPlugin`: Added a new plugin to define the `ILIB_*` constants and supply the iLib locale and resource data for Vite builds, with locale filtering support. +* `ViteWebOSMetaPlugin`: Added a new plugin to emit the webOS `appinfo.json` and its referenced assets for Vite builds. +* `mixins`: Added `applyVite` and the Vite mixins supporting `--isomorphic`, `--framework`/`--externals`, `--snapshot`, and the `--no-minify`/`--verbose`/`--stats` build options. + # 7.0.5 (June 23, 2026) * `option-parser`: Set default theme config to `limestone`. From bd4fd165e6b3d88da980473fb694b85adbc5a8cb Mon Sep 17 00:00:00 2001 From: daniel-stoian-lgp <daniel.stoian@lgepartner.com> Date: Fri, 17 Jul 2026 13:34:53 +0300 Subject: [PATCH 13/13] added changelog --- plugins/ViteHtmlPlugin/README.md | 4 ++-- plugins/ViteILibPlugin/README.md | 6 +++--- plugins/ViteWebOSMetaPlugin/README.md | 2 +- plugins/ViteWebOSMetaPlugin/index.js | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/plugins/ViteHtmlPlugin/README.md b/plugins/ViteHtmlPlugin/README.md index f297db2..3e2699b 100644 --- a/plugins/ViteHtmlPlugin/README.md +++ b/plugins/ViteHtmlPlugin/README.md @@ -4,9 +4,9 @@ Vite counterpart to the `HtmlWebpackPlugin` wiring used by `@enact/cli`. Enact apps have no `index.html`; this plugin synthesizes the document from the same `.ejs` template the webpack build uses and injects the app entry as an ES module. -- **Dev server** — serves the synthesized document at `/`, letting Vite inject +- **Dev server** serves the synthesized document at `/`, letting Vite inject its HMR client via `transformIndexHtml`. -- **Production build** — emits `index.html` referencing the hashed entry chunk +- **Production build** emits `index.html` referencing the hashed entry chunk and linking its CSS, honoring the configured `base` public path. ## Usage diff --git a/plugins/ViteILibPlugin/README.md b/plugins/ViteILibPlugin/README.md index aa75ff6..ff90162 100644 --- a/plugins/ViteILibPlugin/README.md +++ b/plugins/ViteILibPlugin/README.md @@ -7,13 +7,13 @@ available at the URLs they point to. - **Constants** (via the `config` hook → `define`): `ILIB_BASE_PATH`, `ILIB_RESOURCES_PATH`, `ILIB_CACHE_ID`, `ILIB_NO_ASSETS`, per-app/per-theme - `ILIB_<NAME>_PATH`, and optional `ILIB_ADDITIONAL_RESOURCES_PATH` — matching the + `ILIB_<NAME>_PATH`, and optional `ILIB_ADDITIONAL_RESOURCES_PATH` matching the values the webpack ILibPlugin computes. -- **Data** — build: copies the iLib `locale/`, app `resources/`, and theme +- **Data** build: copies the iLib `locale/`, app `resources/`, and theme `resources/` trees into the output on `writeBundle` (a directory copy, not 6.7k individual `emitFile` calls). Dev: serves them from their on-disk source via middleware. -- **Locale filtering** (`locales` option; webpack's `enact pack -l …`) — when set, +- **Locale filtering** (`locales` option; webpack's `enact pack -l …`) when set, the `ilibmanifest.json` file lists are trimmed to the requested locales plus shared (non-locale) data, and a trimmed manifest is emitted/served in place of the full one. Accepts a preset (`used`/`tv`/`signage`/`webos`/`all`), a `.json` diff --git a/plugins/ViteWebOSMetaPlugin/README.md b/plugins/ViteWebOSMetaPlugin/README.md index a45a290..429bc17 100644 --- a/plugins/ViteWebOSMetaPlugin/README.md +++ b/plugins/ViteWebOSMetaPlugin/README.md @@ -3,7 +3,7 @@ Vite counterpart to the webpack `WebOSMetaPlugin`. Discovers the root `appinfo.json` (project root or `./webos-meta/`) and any localized `resources/**/appinfo.json`, and makes them — plus the image assets they -reference (`icon`, `largeIcon`, `splashBackground`, …) — available in the output. +reference (`icon`, `largeIcon`, `splashBackground`, …). - **build** — writes the appinfo file(s) and copies referenced assets into the output on `writeBundle`. diff --git a/plugins/ViteWebOSMetaPlugin/index.js b/plugins/ViteWebOSMetaPlugin/index.js index c49908f..cbfa216 100644 --- a/plugins/ViteWebOSMetaPlugin/index.js +++ b/plugins/ViteWebOSMetaPlugin/index.js @@ -5,7 +5,7 @@ * Vite counterpart to the webpack `WebOSMetaPlugin`. Discovers the root * `appinfo.json` (in the project root or `./webos-meta/`) plus any localized * `resources/**\/appinfo.json`, and makes them — together with the image assets - * they reference (icons, splash, etc.) — available in the output: + * they reference (icons, splash, etc.): * - build → written/copied into the output on `writeBundle`. * - dev → served from computed content / on-disk source via middleware. *