import { spawn } from 'child_process' ; import { promises as fs , existsSync } from 'fs' ; import * as path from 'path' ; import * as svelte from 'svelte/compiler' ; import { PreprocessorGroup } from 'svelte/types/compiler/preprocess' ; import * as chokidar from 'chokidar' ; import * as babel from '@babel/core' ; import * as glob from 'glob' ; import * as terser from 'terser' ; import pLimit from 'p-limit' ; import servor from 'servor' ; import rimraf from 'rimraf' ; import { init as initEsModuleLexer , parse } from 'es-module-lexer' ; import throttle from 'lodash.throttle' ; const IS_PRODUCTION_MODE = process . env . NODE_ENV === 'production' ; const BABEL_CONFIG = loadBabelConfig ( ) ; const SVELTE_PREPROCESSOR_CONFIG = loadSveltePreprocessors ( ) ; function loadBabelConfig ( ) { if ( existsSync ( './babel.config.js' ) ) { return require ( path . join ( process . cwd ( ) , 'babel.config.js' ) ) ; } return { plugins : [ 'svelvet/plugins/babel.js' ] , } ; } function loadSveltePreprocessors ( ) { if ( ! process . argv . includes ( '--preprocess' ) ) return [ ] ; const preprocessConfigPath = process . argv [ process . argv . indexOf ( '--preprocess' ) + 1 ] ; if ( ! existsSync ( preprocessConfigPath ) ) { console . error ( `Cannot find preprocessor: ${ preprocessConfigPath }` ) ; process . exit ( 1 ) ; } return require ( path . join ( process . cwd ( ) , preprocessConfigPath ) ) ; } async function cleanDist ( ) { if ( process . argv . includes ( '--no-clean' ) ) return ; await new Promise ( ( resolve ) => rimraf ( 'public/dist' , resolve ) ) ; } async function compile ( srcPath ) { let logSvelteWarnings = ( ) : void => { } ; try { let source = await fs . readFile ( srcPath , 'utf8' ) ; const isSvelte = srcPath . endsWith ( '.svelte' ) ; if ( isSvelte ) { if ( SVELTE_PREPROCESSOR_CONFIG . length ) { const preprocessed = await svelte . preprocess ( source , SVELTE_PREPROCESSOR_CONFIG , { filename : srcPath , } ) ; source = preprocessed . code ; } const result = svelte . compile ( source , { filename : srcPath , dev : ! IS_PRODUCTION_MODE , hydratable : process . argv . includes ( '--hydratable' ) , immutable : process . argv . includes ( '--immutable' ) , } ) ; logSvelteWarnings = ( ) : void => { result . warnings . forEach ( ( warning ) => { console . log ( '' ) ; console . warn ( '\x1b[33m%s\x1b[0m' , `SVELTE WARNING (${ warning . filename }) -> ${ warning . message }` ) ; console . warn ( warning . frame ) ; } ) ; } ; source = result . js . code ; } const destPath = getDestPath ( srcPath ) . replace ( /.svelte$/ , '.js' ) ; await fs . mkdir ( path . dirname ( destPath ) , { recursive : true } ) ; await fs . writeFile ( destPath , source ) ; console . info ( `Svelte compiled ${ destPath }` ) ; return { destPath , logSvelteWarnings , } ; } catch ( err ) { console . log ( '' ) ; console . error ( `Failed to compile with svelte: ${ srcPath }` ) ; console . error ( err ) ; console . log ( '' ) ; return { destPath : null , logSvelteWarnings , } ; } } function getDestPath ( srcPath ) { return path . normalize ( srcPath ) . replace ( new RegExp ( `^src\\${ path . sep }` ) , `public${ path . sep }dist${ path . sep }` ) ; } async function transform ( destPath , checkModules ) { try { const source = await fs . readFile ( destPath , 'utf8' ) ; let transformed = ( await babel . transformAsync ( source , { ... BABEL_CONFIG , filename : destPath , } ) ) as babel . BabelFileResult ; if ( checkModules ) { const foundMissingWebModule = await checkForNewWebModules ( transformed . code || '' ) ; if ( foundMissingWebModule ) { try { await snowpack ( destPath ) ; } catch ( err ) { console . error ( '\n\nFailed to build with snowpack' ) ; err && console . error ( err . stderr || err ) ; return ; } transformed = ( await babel . transformAsync ( source , BABEL_CONFIG ) ) as babel . BabelFileResult ; } } await fs . writeFile ( destPath , transformed . code ) ; console . info ( `Babel transformed ${ destPath }` ) ; } catch ( err ) { console . log ( '' ) ; console . error ( `Failed to transform with babel: ${ destPath }` ) ; console . error ( err ) ; console . log ( '' ) ; } } async function minify ( destPath ) { try { const source = await fs . readFile ( destPath , 'utf8' ) ; const result = terser . minify ( source , { module : true , } ) ; await fs . writeFile ( destPath , result . code ) ; console . info ( `Terser minified ${ destPath }` ) ; } catch ( err ) { console . log ( '' ) ; console . error ( `Failed to minify with terser: ${ destPath }` ) ; console . error ( err ) ; console . log ( '' ) ; } } async function checkForNewWebModules ( transformedSource ) { await initEsModuleLexer ; const [ esImports ] = parse ( transformedSource ) ; const foundMissingWebModule = esImports . some ( ( meta ) => { const importPath = transformedSource . substring ( meta . s , meta . e ) . replace ( /['"]/g , '' ) ; const notRelative = ! importPath . startsWith ( '.' ) ; const notAbsolute = ! importPath . startsWith ( '/' ) ; const notHttp = ! importPath . startsWith ( 'http://' ) ; const notHttps = ! importPath . startsWith ( 'https://' ) ; return notRelative && notAbsolute && notHttp && notHttps ; } ) ; return foundMissingWebModule ; } async function snowpack ( includeFiles ) { const maybeOptimize = IS_PRODUCTION_MODE ? '--optimize' : '' ; const maybeStats = IS_PRODUCTION_MODE ? '--stat' : '' ; console . info ( `\n\nBuilding web_modules with snowpack...` ) ; const snowpackLocation = path . resolve ( require . resolve ( 'snowpack' ) , '../index.bin.js' ) ; await new Promise ( ( resolve , reject ) => { const proc = spawn ( 'node' , [ snowpackLocation , '--include' , includeFiles , '--dest' , 'public/dist/web_modules' , maybeOptimize , maybeStats , ] , { stdio : 'inherit' , } ) ; proc . on ( 'exit' , ( code ) => { if ( code > 0 ) return reject ( ) ; resolve ( ) ; } ) ; } ) ; console . log ( '\n' ) ; } async function initialBuild ( ) { if ( IS_PRODUCTION_MODE ) console . info ( `Building in production mode...` ) ; const concurrencyLimit = pLimit ( 8 ) ; const globConfig = { nodir : true } ; const svelteAndJsFiles = glob . sync ( 'src/**/!(*+(spec|test)).+(js|mjs|svelte)' , globConfig ) ; const svelteWarnings = [ ] ; const destFiles = await Promise . all ( svelteAndJsFiles . map ( ( srcPath ) => concurrencyLimit ( async ( ) => { const { destPath , logSvelteWarnings } = await compile ( srcPath ) ; svelteWarnings . push ( logSvelteWarnings ) ; return destPath ; } ) ) ) ; try { await snowpack ( 'public/dist/**/*' ) ; } catch ( err ) { console . error ( '\n\nFailed to build with snowpack' ) ; err && console . error ( err . stderr || err ) ; process . exit ( 1 ) ; } await Promise . all ( destFiles . map ( ( destPath ) => concurrencyLimit ( async ( ) => { if ( ! destPath ) return ; await transform ( destPath , false ) ; } ) ) ) ; if ( IS_PRODUCTION_MODE && ! process . argv . includes ( '--no-minify' ) ) { await Promise . all ( destFiles . map ( ( destPath ) => concurrencyLimit ( async ( ) => { if ( ! destPath ) return ; await minify ( destPath ) ; } ) ) ) ; } svelteWarnings . forEach ( ( f ) => f ( ) ) ; } function startWatchMode ( ) { console . info ( `\nWatching for files...` ) ; const handleFile = async ( srcPath ) : Promise < void > => { if ( ! srcPath . endsWith ( '.svelte' ) && ! srcPath . endsWith ( '.js' ) && ! srcPath . endsWith ( '.mjs' ) ) { return ; } const { destPath , logSvelteWarnings } = await compile ( srcPath ) ; if ( ! destPath ) return ; await transform ( destPath , true ) ; logSvelteWarnings ( ) ; } ; const srcWatcher = chokidar . watch ( 'src' , { ignored : /(^|[/\\])\../ , ignoreInitial : true , } ) ; srcWatcher . on ( 'add' , handleFile ) ; srcWatcher . on ( 'change' , throttle ( handleFile , 500 , { trailing : false } ) ) ; } async function startDevServer ( ) { if ( process . argv . includes ( '--no-serve' ) ) return ; let port = 8080 ; if ( process . argv . includes ( '--port' ) ) { port = parseInt ( process . argv [ process . argv . indexOf ( '--port' ) + 1 ] , 10 ) ; } const { url } = await servor ( { root : './public' , fallback : 'index.html' , port , reload : true , } ) ; console . info ( `Server running on ${ url }` ) ; } async function main ( ) { await cleanDist ( ) ; await initialBuild ( ) ; if ( IS_PRODUCTION_MODE ) return ; startWatchMode ( ) ; startDevServer ( ) ; } main ( ) ;