Skip to content

Commit

Permalink
V1.5: experimental esbuild & change/improvements to hydration. (#191)
Browse files Browse the repository at this point in the history
* feat: 🎸 move hydration to be wrapped in requestIdleCallback

* chore: 🤖 v bump

* ci: 🎡 make linter happy

* v1.5 feature: Esbuild for Dev Mode (#187)

* feat: 🎸 modularize rollup plugin for esbuild prep

* feat: 🎸 esbuild working with rollup plugin

* test: 💍 updating tests

* test: 💍 trim defunct tests

* test: 💍 make linter happy

* fix: 🐛 fix type that made dynamic a required key

* feat: 🎸 only bundle components for the client

* try/catch and sourcemap writing

* fix: 🐛 add check for sourcemap before writing

* v bump

* fix: 🐛 support plugins with templates in node_modules

* chore: 🤖 v bump

* feat: 🎸 allow for plugins to use custom layouts

* chore: 🤖 bump before @next release

* ci: 🎡 fix the linter complaining

Co-authored-by: Nick Reese <[email protected]>

* fix: 🐛 change css rebasing relative to distDir not distElder

* fix: 🐛 handle esbuild server race condition

* fix: 🐛 check for components in rollup plugin to prevent failure

* chore: 🤖 v bump

* refactor: 💡 rename conflicting variable in getRollupConfig

* feat: 🎸 PoC of prop compression

* remove devalue

* feat: 🎸 prop compression toggling and debugging in elder.config

* chore: 🤖 v bump

* fix: 🐛 add back in devalue as it is used by rollup

* fix: 🐛 devalue to package.lock

* refactor: 💡 remove stray console log

* fix: 🐛 fix rollup file naming from using indexes

* chore: 🤖 v bump

* fix: 🐛 Fix race condition causing undefined client components

* feat: 🎸 faster prop compression for the client

* fix: 🐛 much, much faster prop compression

* fix: 🐛 prop compression

* fix: 🐛 less buggy prop compression with new Map()

* chore: 🤖 remove commented code

* feat: 🎸 Allow for external file props

* fix: 🐛 Fix promise.all() call and misnamed variable

* chore: 🤖 v bump

* test: 💍 extract propCompression and add tests

* test: 💍 tests for hydrate components

* test: 💍 finish tests

✅ Closes: gp

* ci: 🎡 fix ci

* fix: 🐛 windowsPathFix for prop files

* feat: 🎸 move hydration logic to the browser

* fix: 🐛 fix hydration bug

* chore: 🤖 v bump

* test: 💍 update tests

* feat: 🎸 less hydration code

* chore: 🤖 make linter happy

* fix: 🐛 fix failing tests

* fix: 🐛 add semicolons to end of if statements to prevent issues

* fix: 🐛 1 more semi colon

* chore: 🤖 vbump

* test: 💍 update test to include semi colons

* test: 💍 extract out makeDynamicPermalinkFn

* chore: 🤖 upgrade packages and remove unused

* fix: 🐛 downgrade svelte due to unnamed import error

* chore: 🤖 remove console log

* chore: 🤖 downgrade svelte to stable

* chore: 🤖 bump svelte, remove console log, update tests

* ci: 🎡 make prettier have warings to prevent reformatting

* chore: 🤖 run eslint fix on prettier after solving spacing issue

Co-authored-by: Nick Reese <[email protected]>
  • Loading branch information
nickreese and Nick Reese authored Jun 28, 2021
1 parent 58e027a commit 1d69a4d
Show file tree
Hide file tree
Showing 49 changed files with 5,485 additions and 4,262 deletions.
8 changes: 7 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ module.exports = {
es2020: true,
'jest/globals': true,
},
extends: ['airbnb-base', 'plugin:jest/recommended', 'prettier/@typescript-eslint', 'plugin:prettier/recommended', 'plugin:jest/style'],
extends: [
'airbnb-base',
'plugin:jest/recommended',
'prettier/@typescript-eslint',
'plugin:prettier/recommended',
'plugin:jest/style',
],
settings: {
'import/resolver': {
node: {
Expand Down
3 changes: 2 additions & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
"semi": true,
"trailingComma": "all",
"singleQuote": true,
"printWidth": 120
"printWidth": 120,
"tabWidth": 2
}
4,330 changes: 1,750 additions & 2,580 deletions package-lock.json

Large diffs are not rendered by default.

19 changes: 9 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@elderjs/elderjs",
"version": "1.4.13",
"version": "1.5.0",
"main": "./build/index.js",
"types": "./build/index.d.ts",
"engineStrict": true,
Expand All @@ -25,42 +25,41 @@
},
"dependencies": {
"@babel/core": "^7.13.10",
"@babel/preset-env": "^7.13.10",
"@elderjs/shortcodes": "^1.0.7",
"@rollup/plugin-commonjs": "^17.1.0",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^11.2.0",
"@rollup/plugin-replace": "^2.4.1",
"@rollup/plugin-replace": "^2.4.2",
"btoa": "^1.2.1",
"chokidar": "^3.5.1",
"clean-css": "^5.1.1",
"cli-progress": "^3.9.0",
"core-js": "^3.9.1",
"cosmiconfig": "^7.0.0",
"del": "^6.0.0",
"devalue": "^2.0.1",
"esbuild": "^0.12.4",
"fs-extra": "^9.1.0",
"glob": "^7.1.6",
"lodash.defaultsdeep": "^4.6.1",
"lodash.get": "^4.4.2",
"lodash.kebabcase": "^4.1.1",
"nanoid": "^3.1.20",
"regexparam": "^1.3.0",
"rollup": "^2.41.0",
"regexparam": "^2.0.0",
"rollup": "^2.51.2",
"rollup-plugin-babel": "^4.4.0",
"rollup-plugin-multi-input": "^1.2.0",
"rollup-plugin-terser": "^7.0.2",
"route-sort": "^1.0.0",
"spark-md5": "^3.0.1",
"svelte": "3.35.0",
"svelte": "^3.38.3",
"yup": "^0.29.3"
},
"devDependencies": {
"@types/fs-extra": "^9.0.8",
"@types/jest": "^26.0.20",
"@types/node": "^14.14.33",
"@typescript-eslint/eslint-plugin": "^4.17.0",
"@typescript-eslint/parser": "^4.17.0",
"@typescript-eslint/eslint-plugin": "^4.28.1",
"@typescript-eslint/parser": "^4.28.1",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^7.21.0",
"eslint-config-airbnb-base": "^14.2.1",
Expand All @@ -70,7 +69,7 @@
"eslint-plugin-prettier": "^3.1.4",
"jest": "^26.6.3",
"locate-character": "^2.0.5",
"prettier": "^2.1.2",
"prettier": "^2.3.2",
"rimraf": "^3.0.2",
"ts-jest": "^26.5.3",
"ts-node": "^9.1.1",
Expand Down
4 changes: 2 additions & 2 deletions src/Elder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ class Elder {
// validate hooks
this.hooks = [...elderJsHooks, ...pluginHooks, ...hooksJs]
.map((hook) => validateHook(hook))
.filter((Boolean as any) as ExcludesFalse);
.filter(Boolean as any as ExcludesFalse);

if (this.settings.hooks.disable && this.settings.hooks.disable.length > 0) {
this.hooks = this.hooks.filter((h) => !this.settings.hooks.disable.includes(h.name));
Expand Down Expand Up @@ -201,7 +201,7 @@ class Elder {
// validate shortcodes
this.shortcodes = [...elderJsShortcodes, ...pluginShortcodes, ...shortcodesJs]
.map((shortcode) => validateShortcode(shortcode))
.filter((Boolean as any) as ExcludesFalse);
.filter(Boolean as any as ExcludesFalse);

/**
*
Expand Down
20 changes: 11 additions & 9 deletions src/__tests__/Elder.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,18 +50,20 @@ describe('#Elder', () => {

jest.mock(`..${sep}workerBuild`);

jest.mock(`..${sep}utils${sep}prepareRunHook`, () => (page) =>
async function processHook(hook) {
if (hook === 'bootstrap' && page.hooks && page.hooks.length) {
for (const pluginHook of page.hooks) {
if (pluginHook.$$meta.type === 'plugin') {
// eslint-disable-next-line
jest.mock(
`..${sep}utils${sep}prepareRunHook`,
() => (page) =>
async function processHook(hook) {
if (hook === 'bootstrap' && page.hooks && page.hooks.length) {
for (const pluginHook of page.hooks) {
if (pluginHook.$$meta.type === 'plugin') {
// eslint-disable-next-line
await pluginHook.run({});
}
}
}
}
return null;
},
return null;
},
);
beforeEach(() => jest.resetModules());

Expand Down
4 changes: 2 additions & 2 deletions src/__tests__/__snapshots__/Elder.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -495,7 +495,7 @@ Object {
"addedBy": "elder.js",
"type": "internal",
},
"description": "Sets up the default polyfill for the intersection observer",
"description": "Sets up the default polyfill for the intersection observer and request idle callback.",
"hook": "stacks",
"name": "elderAddDefaultIntersectionObserver",
"priority": 100,
Expand Down Expand Up @@ -1148,7 +1148,7 @@ Object {
"addedBy": "elder.js",
"type": "internal",
},
"description": "Sets up the default polyfill for the intersection observer",
"description": "Sets up the default polyfill for the intersection observer and request idle callback.",
"hook": "stacks",
"name": "elderAddDefaultIntersectionObserver",
"priority": 100,
Expand Down
228 changes: 228 additions & 0 deletions src/esbuild/esbuildBundler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
/* eslint-disable import/no-dynamic-require */
// reload the build process when svelte files are added clearing the cache.

// server that reloads the app which watches the file system for changes.
// reload can also be called after esbuild finishes the rebuild.
// the file watcher should restart the entire esbuild process when a new svelte file is seen. This includes clearing caches.

import { build, BuildResult, buildSync } from 'esbuild';
import glob from 'glob';
import path from 'path';

import fs from 'fs-extra';

// eslint-disable-next-line import/no-unresolved
import { PreprocessorGroup } from 'svelte/types/compiler/preprocess/types';
import esbuildPluginSvelte from './esbuildPluginSvelte';
import { InitializationOptions, SettingsOptions } from '../utils/types';
import { getElderConfig } from '..';
import { devServer } from '../rollup/rollupPlugin';
import getPluginLocations from '../utils/getPluginLocations';

const production = process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'PRODUCTION';
export type TPreprocess = PreprocessorGroup | PreprocessorGroup[] | false;
export type TSvelteHandler = {
config: SettingsOptions;
preprocess: TPreprocess;
};

export function getSvelteConfig(elderConfig: SettingsOptions): TPreprocess {
const svelteConfigPath = path.resolve(elderConfig.rootDir, `./svelte.config.js`);
if (fs.existsSync(svelteConfigPath)) {
try {
// eslint-disable-next-line import/no-dynamic-require
const req = require(svelteConfigPath);
if (req) {
return req;
}
} catch (err) {
if (err.code === 'MODULE_NOT_FOUND') {
console.warn(`Unable to load svelte.config.js from ${svelteConfigPath}`, err);
}
return false;
}
}
return false;
}

export function getPackagesWithSvelte(pkg, elderConfig: SettingsOptions) {
const pkgs = []
.concat(pkg.dependents ? Object.keys(pkg.dependents) : [])
.concat(pkg.devDependencies ? Object.keys(pkg.devDependencies) : []);
const sveltePackages = pkgs.reduce((out, cv) => {
try {
const resolved = path.resolve(elderConfig.rootDir, `./node_modules/${cv}/package.json`);
const current = require(resolved);
if (current.svelte) {
out.push(cv);
}
} catch (e) {
//
}
return out;
}, []);
return sveltePackages;
}

const getRestartHelper = (startOrRestartServer) => {
let state;
const defaultState = { ssr: false, client: false };
const resetState = () => {
state = JSON.parse(JSON.stringify(defaultState));
};

resetState();

// eslint-disable-next-line consistent-return
return (type: 'start' | 'reset' | 'client' | 'ssr') => {
if (type === 'start') {
return startOrRestartServer();
}
if (type === 'reset') {
return resetState();
}

state[type] = true;
if (state.ssr && state.client) {
startOrRestartServer();
resetState();
}
};
};

const svelteHandler = async ({ elderConfig, svelteConfig, replacements, restartHelper }) => {
const builders: { ssr?: BuildResult; client?: BuildResult } = {};

// eslint-disable-next-line global-require
const pkg = require(path.resolve(elderConfig.rootDir, './package.json'));
const globPath = path.resolve(elderConfig.rootDir, `./src/**/*.svelte`);
const initialEntryPoints = glob.sync(globPath);
const sveltePackages = getPackagesWithSvelte(pkg, elderConfig);
const elderPlugins = getPluginLocations(elderConfig);

builders.ssr = await build({
entryPoints: [...initialEntryPoints, ...elderPlugins.files],
bundle: true,
outdir: elderConfig.$$internal.ssrComponents,
plugins: [
esbuildPluginSvelte({
type: 'ssr',
sveltePackages,
elderConfig,
svelteConfig,
}),
],
watch: {
onRebuild(error) {
restartHelper('ssr');
if (error) console.error('ssr watch build failed:', error);
},
},
format: 'cjs',
target: ['node12'],
platform: 'node',
sourcemap: !production,
minify: production,
outbase: 'src',
external: pkg.dependents ? [...Object.keys(pkg.dependents)] : [],
define: {
'process.env.componentType': "'server'",
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
...replacements,
},
});

builders.client = await build({
entryPoints: [...initialEntryPoints.filter((i) => i.includes('src/components')), ...elderPlugins.files],
bundle: true,
outdir: elderConfig.$$internal.clientComponents,
entryNames: '[dir]/[name].[hash]',
plugins: [
esbuildPluginSvelte({
type: 'client',
sveltePackages,
elderConfig,
svelteConfig,
}),
],
watch: {
onRebuild(error) {
if (error) console.error('client watch build failed:', error);
restartHelper('client');
},
},
format: 'esm',
target: ['es2020'],
platform: 'node',
sourcemap: !production,
minify: true,
splitting: true,
chunkNames: 'chunks/[name].[hash]',
logLevel: 'error',
outbase: 'src',
define: {
'process.env.componentType': "'browser'",
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
...replacements,
},
});

restartHelper('start');

const restart = async () => {
if (builders.ssr) await builders.ssr.stop();
if (builders.client) await builders.client.stop();
restartHelper('reset');
return svelteHandler({
elderConfig,
svelteConfig,
replacements,
restartHelper,
});
};

return restart;
};

type TEsbuildBundler = {
initializationOptions?: InitializationOptions;
replacements?: { [key: string]: string | boolean };
};

const esbuildBundler = async ({ initializationOptions = {}, replacements = {} }: TEsbuildBundler = {}) => {
const elderConfig = getElderConfig(initializationOptions);
const svelteConfig = getSvelteConfig(elderConfig);

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { startOrRestartServer, startWatcher, childProcess } = devServer({
elderConfig,
});

const restartHelper = getRestartHelper(startOrRestartServer);

if (!fs.existsSync(path.resolve('./node_modules/intersection-observer/intersection-observer.js'))) {
throw new Error(`Missing 'intersection-observer' dependency. Run 'npm i --save intersection-observer' to fix.`);
}

buildSync({
format: 'iife',
minify: true,
watch: false,
outfile: path.resolve(
elderConfig.prefix ? path.join(elderConfig.distDir, elderConfig.prefix) : elderConfig.distDir,
`./static/intersection-observer.js`,
),
entryPoints: [path.resolve('./node_modules/intersection-observer/intersection-observer.js')],
});

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const restartEsbuild = await svelteHandler({
elderConfig,
svelteConfig,
replacements,
restartHelper,
});

startWatcher();
};
export default esbuildBundler;
Loading

0 comments on commit 1d69a4d

Please sign in to comment.