Skip to content

Instantly share code, notes, and snippets.

@misterboe
Last active August 2, 2023 06:44
Show Gist options
  • Save misterboe/c9ec23e5969a7dc1b371440c75bc71da to your computer and use it in GitHub Desktop.
Save misterboe/c9ec23e5969a7dc1b371440c75bc71da to your computer and use it in GitHub Desktop.
Using Vite in typo3 provider
<?php
declare(strict_types=1);
namespace Bo\Bvhs\ViewHelpers\Vite;
use B13\Assetcollector\AssetCollector;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\PathUtility;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
/**
* MainViewHelper integrates Vite into TYPO3, allowing the use of Vite's build system and development server.
*
* Example usage:
* <b:vite.main outdir="fileadmin/sitepackage/Resources/Public" input="src/main.js" port="5999" additionalAttributes="{data-cookieconsent: 'ignore'}" />
*
* Arguments:
* - outdir (string, required): The Vite output directory (manifest.json location from sitepackage root).
* - input (string, required): The Vite main entry file (main.js location from sitepackage root).
* - port (int, optional): The port for the Vite development server (default: 5999).
* - additionalAttributes (array, optional): Additional attributes for the script and link tags.
*
* The ViewHelper will automatically detect if you are in Vite development mode by checking the VUE_DEVELOPMENT
* environment variable. If it's set to true, the ViewHelper will include the Vite client script and main entry
* script from the Vite development server. Otherwise, it will include the built JavaScript and CSS files
* generated by Vite from the output directory.
*/
class MainViewHelper extends AbstractViewHelper
{
protected AssetCollector $assetCollector;
public function __construct(AssetCollector $assetCollector)
{
$this->assetCollector = $assetCollector;
}
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument(
"outdir",
"string",
"Vite output directory (manifest.json location from sitepackage root)",
true
);
$this->registerArgument(
"input",
"string",
"Vite main entry file (main.js location from sitepackage root)",
true
);
$this->registerArgument(
"port",
"int",
"Optional: Port for the Vite development server (default: 5999)",
false,
5999
);
$this->registerArgument(
"additionalAttributes",
"array",
"Optional: Additional attributes for the script and link tags",
false,
[]
);
}
public function render(): void
{
$currentApplicationContext = \TYPO3\CMS\Core\Core\Environment::getContext()->__toString();
$absoluteOutdir = GeneralUtility::getFileAbsFileName($this->arguments["outdir"]);
$relativeOutdir = PathUtility::getAbsoluteWebPath($absoluteOutdir);
$vueDevelopment = getenv("VUE_DEVELOPMENT");
$viteDevelopment = getenv("VITE_DEVELOPMENT");
$additionalAttributes = $this->arguments["additionalAttributes"];
$additionalAttributesString = "";
foreach ($additionalAttributes as $key => $value) {
$additionalAttributesString .= ' ' . htmlspecialchars($key) . '="' . htmlspecialchars($value) . '"';
}
if ($viteDevelopment == true || $vueDevelopment == true) {
$pageRenderer = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Page\PageRenderer::class);
$pageRenderer->addHeaderData(
'<script type="module" src="http://localhost:' .
$this->arguments["port"] .
"/" .
'@vite/client"' .
$additionalAttributesString .
'></script>'
);
$pageRenderer->addHeaderData(
'<script type="module" src="http://localhost:' .
$this->arguments["port"] .
"/" .
$this->arguments["input"] .
'"' .
$additionalAttributesString .
'></script>'
);
} else {
$file = file_get_contents($absoluteOutdir . "/manifest.json");
$manifest = json_decode($file, true);
if (!str_ends_with($relativeOutdir, "/")) {
$relativeOutdir .= "/";
}
if (!isset($manifest[$this->arguments["input"]])) {
return;
}
$mainFile = $relativeOutdir . $manifest[$this->arguments["input"]]["file"];
if ($mainFile) {
$fileExtension = pathinfo($mainFile, PATHINFO_EXTENSION);
switch ($fileExtension) {
case 'js':
$this->assetCollector->addJavaScriptFile($mainFile, array_merge(["type" => "module", "async" => "true"], $additionalAttributes));
break;
case 'css':
if (str_starts_with($mainFile, "/")) {
$mainFile = substr($mainFile, 1);
}
$this->assetCollector->addCssFile($mainFile, $additionalAttributes);
break;
}
}
if (isset($manifest[$this->arguments["input"]]["css"])) {
foreach ($manifest[$this->arguments["input"]]["css"] as $maincssfile) {
if (str_starts_with($relativeOutdir, "/")) {
$relativeOutdir = substr($relativeOutdir, 1);
}
$this->assetCollector->addCssFile($relativeOutdir . $maincssfile, $additionalAttributes);
}
}
}
}
}

package.json in sitepackage

{  
	"name": "site_package",  
	"private": true,  
	"type": "module",  
	"version": "0.0.0",  
	"scripts": {  
	"preinstall": "n auto",  
	"ce-linker": "spb-unlink && spb-link",  
	"dev": "vite --mode development",  
	"build": "vite build --mode production",  
	"build:watch": "vite build --watch --mode production",  
	"preview": "vite preview --port 4173 --mode production",  
	"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore", 
	"postinstall": "vite build --mode production"  
	},  
	"devDependencies": {  
		"@analog-digitalagentur/content-element-linker": "1.0.0",  
		"@analog-digitalagentur/vite-shared-config": "^0.0.7"  
	},  
	"config": {  
		"content_element_path": "Modules/ContentElements",  
		"extension_name": "site_package"  
	},  
	"settings": {  
		"vite": {  
		"vendor": "analogde",  
		"extName": "site-package",  
		"assetsDir": "Assets",  
		"hashedAssetDir": true,  
		"port": 5111  
		}  
	}  
}

vite.config.js in sitepackage

import fs from 'fs'  
import path from 'path'  
import {generateViteConfig} from '@analog-digitalagentur/vite-shared-config'  
import { fileURLToPath } from 'url';  
  
const __dirname = path.dirname(fileURLToPath(import.meta.url));  
  
// Get the project root directory  
const projectRoot = path.resolve(__dirname, './')  
  
// Read the package.json  
const packageJson = JSON.parse(fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf-8'))  
  
// Extract the settings  
const settings = packageJson.settings.vite  
  
export default generateViteConfig(settings)

vite-shared-config/vite.config.js

// Import the required modules
import { defineConfig } from 'vite'
import path from 'path'
import { ViteFluid } from './vite.fluid.js'
import { ViteCleanUp } from './vite.cleanUp.js'
import copy from 'rollup-plugin-copy'
import { terser } from 'rollup-plugin-terser'
import fs from 'fs'
import crypto from 'crypto'
import autoprefixer from 'autoprefixer';
import { fileURLToPath } from 'url';
const __dirname = fileURLToPath(import.meta.url);

export function generateViteConfig(settings) {
  const sourcePath = path.join(process.cwd(), './Modules')
  const stylePath = path.join(process.cwd(), './Modules/ContentElements')
  const stylePathSub = path.join(process.cwd(), './Modules/GlobalTemplates/SubParts')

  let buildFiles = {}

  const walkSync = (dir, filelist = []) => {
    fs.readdirSync(dir).forEach((file) => {
      const dirFile = path.join(dir, file)
      try {
        filelist = walkSync(dirFile, filelist)
      } catch (err) {
        if (err.code === 'ENOTDIR' || err.code === 'EBUSY') filelist = [...filelist, dirFile]
        else throw err
      }
    })
    return filelist
  }
  const scssFiles = walkSync(stylePath).filter((file) => file.match(/\\.scss$/))
  const scssFilesSub = walkSync(stylePathSub).filter((file) => file.match(/\\.scss$/))

  scssFiles.forEach((scssFile) => {
    const fileName = path.basename(scssFile, '.scss')
    buildFiles[fileName] = scssFile
  })

  scssFilesSub.forEach((scssFile) => {
    const fileName = path.basename(scssFile, '.scss')
    buildFiles[fileName] = scssFile
  })

  buildFiles.main = './Modules/GlobalTemplates/main.js'

  console.log('buildFiles', buildFiles)

  const mode = process.argv[process.argv.length - 1]

  const extPath = `/vendor/${settings.vendor}/${settings.extName}/`
  const hash = crypto.createHash('md5').update(extPath).digest('hex')
  const hashedAssetDir = '/_assets/' + hash + '/' + settings.assetsDir + '/'

  console.log('Vite is running in ' + mode + ' mode')
  console.log('Vite is using the following settings:', settings)

  let productionPath = `/typo3conf/ext/${settings.extName}/Resources/Public/${settings.assetsDir}/`

  if (settings.hashedAssetDir) {
    productionPath = hashedAssetDir
  }

  console.log('Vite is using the following public path:', productionPath)
  console.log('')

  return defineConfig({
    base: mode === 'production' ? productionPath : './',
    plugins: [
      ViteFluid(),
      ViteCleanUp(),
      copy({
        targets: [
          {
            src: path.resolve(sourcePath, 'GlobalTemplates/Assets/*'),
            dest: path.resolve(process.cwd(), './Resources/Public/Modules/Assets'),
          },
          {
            src: path.resolve(sourcePath, 'GlobalTemplates/Favicon/*'),
            dest: path.resolve(process.cwd(), './Resources/Public/Modules/Assets/Favicon'),
          },
        ]
      }),
      mode === 'production' && terser(),
    ],
    server: {
      port: settings.port,
      origin: 'http://localhost:' + settings.port,
      https: false,
      hmr: {
        host: 'localhost',
        protocol: 'ws',
      },
    },
    resolve: {
      alias: {
        '@': path.resolve(__dirname, 'src'),
      }
    },
    publicDir: false,
    build: {
      emptyOutDir: true,
      manifest: true,
      minify: true,
      sourcemap: mode === 'development',
      outDir: path.resolve(process.cwd(), `./Resources/Public/${settings.assetsDir}`),
      rollupOptions: {
        input: buildFiles
      }
    },
    css: {
      devSourcemap: true,
      postcss: {
        plugins: [
          autoprefixer({
            overrideBrowserslist: ['defaults'],
          }),
        ],
      }
    },
    esbuild: {
      minify: mode === 'production',
      target: 'es2015'
    }
  })
}

vite-shared-config/vite.cleanUp.js

export function ViteCleanUp() {
  return {
    name: 'remove-empty-chunks',
    generateBundle(options, bundle) {
      for (const name in bundle) {
        const file = bundle[name];
        if (file.type === 'chunk' && (!file.code || file.code.trim() === '')) {
          delete bundle[name];
        }
      }
    }
  };
}

vite-shared-config/vite.fluid.js

export function ViteFluid() {
  return {
    name: "ViteFluid",
    enforce: "post",
    configureServer(server) {
      const fullReload = () => {
        server.ws.send({ type: "full-reload", path: "*" });
      };
      const hotReload = (file) => {
        server.ws.send({ type: "update", updates: [{ type: "update", acceptedPath: file }] });
      };

      server.watcher.on("add", fullReload);
      server.watcher.on("change", (file) => {
        console.log(file);
        if (
            file.endsWith(".html") ||
            file.endsWith(".typoscript") ||
            file.endsWith(".yaml") ||
            file.endsWith(".php")
        ) {
          fullReload();
        } else {
          hotReload(file);
        }
      });
    },
  };
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment