diff --git a/tests/src/FullStackLifecycle.test.js b/tests/src/FullStackLifecycle.test.js
index cbdd24e0..abf123d9 100644
--- a/tests/src/FullStackLifecycle.test.js
+++ b/tests/src/FullStackLifecycle.test.js
@@ -62,7 +62,9 @@ describe('FullStackLifecycle ssr', () => {
describe('FullStackLifecycle spa', () => {
beforeAll(async () => {
await page.goto('http://localhost:6969/')
+ await page.waitForSelector('[data-application-hydrated]')
await page.click('a[href="/full-stack-lifecycle"]')
+ await page.waitForSelector('[data-hydrated]')
})
test('prepare should run', async () => {
diff --git a/tests/src/LazyComponent.njs b/tests/src/LazyComponent.njs
index 7ffb1a06..62b6605c 100644
--- a/tests/src/LazyComponent.njs
+++ b/tests/src/LazyComponent.njs
@@ -11,11 +11,12 @@ class LazyComponent extends Nullstack {
this.safelisted = await this.serverFunctionWorks()
}
- render() {
+ render({ prop }) {
return (
-
- {' '}
- LazyComponent{' '}
+
+
safelisted: {this.safelisted}
+
prop: {prop}
+
home
)
}
diff --git a/tests/src/LazyComponentLoader.njs b/tests/src/LazyComponentLoader.njs
index 7ad55b99..57702b76 100644
--- a/tests/src/LazyComponentLoader.njs
+++ b/tests/src/LazyComponentLoader.njs
@@ -15,7 +15,7 @@ class LazyComponentLoader extends Nullstack {
}
render() {
- if (!this.hydrated) return false
+ if (!LazyComponent) return false
return
}
diff --git a/tests/src/Logo.njs b/tests/src/Logo.njs
new file mode 100644
index 00000000..dfefe3d6
--- /dev/null
+++ b/tests/src/Logo.njs
@@ -0,0 +1,16 @@
+import Nullstack from 'nullstack';
+import NullstackLogo from 'nullstack/logo';
+
+class Logo extends Nullstack {
+
+ render() {
+ return (
+
+
+
+ )
+ }
+
+}
+
+export default Logo;
\ No newline at end of file
diff --git a/tests/src/Logo.test.js b/tests/src/Logo.test.js
new file mode 100644
index 00000000..d56d56c6
--- /dev/null
+++ b/tests/src/Logo.test.js
@@ -0,0 +1,10 @@
+beforeAll(async () => {
+ await page.goto('http://localhost:6969/logo')
+})
+
+describe('Logo', () => {
+ test('logo can be imported', async () => {
+ const element = await page.$('svg[viewBox="0 0 511.5039 113.7368"]')
+ expect(element).toBeTruthy()
+ })
+})
\ No newline at end of file
diff --git a/tests/src/RenderableComponent.njs b/tests/src/RenderableComponent.njs
index 59a00f25..4b8587b6 100644
--- a/tests/src/RenderableComponent.njs
+++ b/tests/src/RenderableComponent.njs
@@ -25,6 +25,10 @@ class RenderableComponent extends Nullstack {
return false
}
+ renderRepeated({ number }) {
+ return
+ }
+
render({ params }) {
const list = params.shortList ? [1, 2, 3] : [1, 2, 3, 4, 5, 6]
const html = '
Nullstack '
@@ -33,7 +37,7 @@ class RenderableComponent extends Nullstack {
-
+
this is a normal tag
@@ -43,7 +47,7 @@ class RenderableComponent extends Nullstack {
element tag
-
+
children
@@ -65,13 +69,15 @@ class RenderableComponent extends Nullstack {
-
)
}
}
-export default RenderableComponent
+export default RenderableComponent
\ No newline at end of file
diff --git a/tests/src/RenderableComponent.test.js b/tests/src/RenderableComponent.test.js
index c2a63b50..c8d9a77d 100644
--- a/tests/src/RenderableComponent.test.js
+++ b/tests/src/RenderableComponent.test.js
@@ -87,6 +87,12 @@ describe('RenderableComponent', () => {
const element = await page.$('[data-reference]')
expect(element).toBeTruthy()
})
+
+ test('inner components can be repeated', async () => {
+ const first = await page.$('[data-repeated="1"]')
+ const second = await page.$('[data-repeated="2"]')
+ expect(first && second).toBeTruthy()
+ })
})
describe('RenderableComponent ?condition=true', () => {
diff --git a/tests/src/RoutesAndParams.test.js b/tests/src/RoutesAndParams.test.js
index 26003cd9..5af07122 100644
--- a/tests/src/RoutesAndParams.test.js
+++ b/tests/src/RoutesAndParams.test.js
@@ -258,22 +258,26 @@ describe('RoutesAndParams /routes-and-params/inner-html', () => {
describe('RoutesAndParams /routes-and-params/hrefs spa', () => {
beforeEach(async () => {
await page.goto('http://localhost:6969/routes-and-params/hrefs')
+ await page.waitForSelector(`[data-application-hydrated]`)
})
test('https urls do a full redirect', async () => {
- await Promise.all([page.click('[href="https://nullstack.app/"]'), page.waitForNavigation()])
+ await page.click('[href="https://nullstack.app/"]')
+ await page.waitForSelector('link[rel="canonical"][href="https://nullstack.app/"]')
const url = await page.url()
expect(url).toMatch('://nullstack.app')
})
test('http urls do a full redirect', async () => {
- await Promise.all([page.click('[href="http://nullstack.app/"]'), page.waitForNavigation()])
+ await page.click('[href="http://nullstack.app/"]')
+ await page.waitForSelector('link[rel="canonical"][href="https://nullstack.app/"]')
const url = await page.url()
expect(url).toMatch('://nullstack.app')
})
test('// urls do a full redirect', async () => {
- await Promise.all([page.click('[href="//nullstack.app/"]'), page.waitForNavigation()])
+ await page.click('[href="//nullstack.app/"]')
+ await page.waitForSelector('link[rel="canonical"][href="https://nullstack.app/"]')
const url = await page.url()
expect(url).toMatch('://nullstack.app')
})
diff --git a/tests/src/ServerFunctions.njs b/tests/src/ServerFunctions.njs
index f526116d..d5081cce 100644
--- a/tests/src/ServerFunctions.njs
+++ b/tests/src/ServerFunctions.njs
@@ -62,6 +62,10 @@ class ServerFunctions extends Nullstack {
return true
}
+ static async getPrivateFunction({ request }) {
+ return this._privateFunction()
+ }
+
static async getRequestUrl({ request }) {
return request.originalUrl.startsWith('/')
}
@@ -75,6 +79,7 @@ class ServerFunctions extends Nullstack {
async hydrate() {
this.underlineRemovedFromClient = !ServerFunctions._privateFunction
+ this.underlineStayOnServer = await ServerFunctions.getPrivateFunction()
this.doublePlusOneClient = await ServerFunctions.getDoublePlusOne({ number: 34 })
this.acceptsSpecialCharacters = await this.getEncodedString({ string: decodedString })
this.hydratedOriginalUrl = await ServerFunctions.getRequestUrl()
@@ -100,6 +105,7 @@ class ServerFunctions extends Nullstack {
+
diff --git a/tests/src/ServerFunctions.test.js b/tests/src/ServerFunctions.test.js
index 329902a5..32e28dba 100644
--- a/tests/src/ServerFunctions.test.js
+++ b/tests/src/ServerFunctions.test.js
@@ -63,6 +63,12 @@ describe('ServerFunctions', () => {
expect(element).toBeTruthy()
})
+ test('server functions starting with underline stay on server', async () => {
+ await page.waitForSelector('[data-underline-stay-on-server]')
+ const element = await page.$('[data-underline-stay-on-server]')
+ expect(element).toBeTruthy()
+ })
+
test('static server functions receive the context on spa', async () => {
await page.waitForSelector('[data-hydrated-original-url]')
const element = await page.$('[data-hydrated-original-url]')
diff --git a/tests/src/WindowDependency.js b/tests/src/WindowDependency.js
deleted file mode 100644
index b1806955..00000000
--- a/tests/src/WindowDependency.js
+++ /dev/null
@@ -1,3 +0,0 @@
-window.key = 'shim'
-
-export default window
diff --git a/tests/src/externalRoute.js b/tests/src/externalRoute.js
new file mode 100644
index 00000000..f9d25d97
--- /dev/null
+++ b/tests/src/externalRoute.js
@@ -0,0 +1,8 @@
+export default function setExternalRoute(server) {
+ server.get('/external-route.json', (_request, response) => {
+ response.json({ nice: 69 })
+ })
+ server.get('/external-route.json', (_request, response) => {
+ response.json({ nicent: 68 })
+ })
+}
diff --git a/tests/src/externalRoute.test.js b/tests/src/externalRoute.test.js
new file mode 100644
index 00000000..48d1fb77
--- /dev/null
+++ b/tests/src/externalRoute.test.js
@@ -0,0 +1,12 @@
+beforeAll(async () => {
+ await page.goto('http://localhost:6969/external-route.json')
+})
+
+describe('ExtermalRoute', () => {
+ test('express functions keep their priority', async () => {
+ const response = await page.evaluate(() => {
+ return JSON.parse(document.querySelector('body').innerText)
+ })
+ expect(response.nice === 69).toBeTruthy()
+ })
+})
diff --git a/tests/src/nested/NestedFolder.njs b/tests/src/nested/NestedFolder.njs
new file mode 100644
index 00000000..fac716cc
--- /dev/null
+++ b/tests/src/nested/NestedFolder.njs
@@ -0,0 +1,17 @@
+import Nullstack from 'nullstack'
+import LazyComponent from '../LazyComponent'
+
+class NestedFolder extends Nullstack {
+
+ render() {
+ return (
+
+ NestedFolder
+
+
+ )
+ }
+
+}
+
+export default NestedFolder;
\ No newline at end of file
diff --git a/tests/webpack.config.js b/tests/webpack.config.js
index dcbc7a72..f399c2d5 100644
--- a/tests/webpack.config.js
+++ b/tests/webpack.config.js
@@ -1,22 +1,27 @@
-const [server, client] = require('nullstack/webpack.config')
-
const glob = require('glob')
-const PurgecssPlugin = require('purgecss-webpack-plugin')
+const path = require('path')
+const { PurgeCSSPlugin } = require('purgecss-webpack-plugin')
-function customClient(...args) {
- const config = client(...args)
- if (config.mode === 'production') {
- config.plugins.push(
- new PurgecssPlugin({
- paths: glob.sync(`src/**/*`, { nodir: true }),
- content: ['./**/*.njs'],
- safelist: ['script', 'body', 'html', 'style'],
- defaultExtractor: (content) => content.match(/[^<>"'`\s]*[^<>"'`\s:]/g) || [],
- }),
- )
- }
+const [server, client] = require('../webpack.config')
- return config
+function applyAliases(environments) {
+ return environments.map((environment) => (...args) => {
+ const config = environment(...args)
+ config.resolve.alias._ = path.join(process.cwd(), '..', 'node_modules');
+ config.resolve.alias["terser"] = path.join(process.cwd(), '..', 'node_modules', '@swc/core');
+ config.resolve.alias.webpack = path.join(process.cwd(), '..', 'node_modules', 'webpack');
+ if (config.mode === 'production' && config.target === 'web') {
+ config.plugins.push(
+ new PurgeCSSPlugin({
+ paths: glob.sync(`src/**/*`, { nodir: true }),
+ content: ['./**/*.njs'],
+ safelist: ['script', 'body', 'html', 'style'],
+ defaultExtractor: (content) => content.match(/[^<>"'`\s]*[^<>"'`\s:]/g) || [],
+ }),
+ )
+ }
+ return config
+ })
}
-module.exports = [server, customClient]
+module.exports = applyAliases([server, client])
diff --git a/types/Environment.d.ts b/types/Environment.d.ts
index 70bebfe5..db962150 100644
--- a/types/Environment.d.ts
+++ b/types/Environment.d.ts
@@ -24,7 +24,5 @@ export type NullstackEnvironment = {
*/
event: string
- hot?: boolean
-
disk?: boolean
}
diff --git a/webpack.config.js b/webpack.config.js
index a6cd8aab..4904a02f 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -1,465 +1,57 @@
-const CopyPlugin = require('copy-webpack-plugin')
-const crypto = require('crypto')
-const { existsSync, readdirSync } = require('fs')
-const MiniCssExtractPlugin = require('mini-css-extract-plugin')
-const NodemonPlugin = require('nodemon-webpack-plugin')
+const { existsSync } = require('fs')
const path = require('path')
-const TerserPlugin = require('terser-webpack-plugin')
-const buildKey = crypto.randomBytes(20).toString('hex')
-
-const customConsole = new Proxy(
- {},
- {
- get() {
- return () => {}
- },
- },
-)
-
-function getLoader(loader) {
- const loaders = path.resolve('./node_modules/nullstack/loaders')
- return path.join(loaders, loader)
-}
-
-function cacheFactory(args, folder, name) {
- if (args.cache || args.environment === 'development') {
- return {
- type: 'filesystem',
- cacheDirectory: path.resolve(`./${folder}/.cache`),
- name,
- }
+function getOptions(target, options) {
+ const disk = !!options.disk;
+ const environment = options.environment
+ const entry = existsSync(path.posix.join(process.cwd(), `${target}.ts`)) ? `./${target}.ts` : `./${target}.js`
+ const projectFolder = process.cwd()
+ const configFolder = __dirname
+ const buildFolder = '.' + environment
+ const cache = !options.skipCache
+ const name = options.name || ''
+ const trace = !!options.trace
+ return {
+ target,
+ disk,
+ buildFolder,
+ entry,
+ environment,
+ cache,
+ name,
+ trace,
+ projectFolder,
+ configFolder
}
- return false
-}
-
-function terserMinimizer(file, _sourceMap) {
- return require('@swc/core').minify(file, {
- keepClassnames: true,
- keepFnames: true,
- sourceMap: true,
- })
-}
-
-const swcJs = {
- test: /\.js$/,
- use: {
- loader: require.resolve('swc-loader'),
- options: {
- jsc: {
- parser: {
- syntax: 'ecmascript',
- exportDefaultFrom: true,
- },
- },
- env: {
- targets: { node: '10' },
- },
- },
- },
-}
-
-const babelJs = {
- test: /\.js$/,
- resolve: {
- extensions: ['.njs', '.js', '.nts', '.ts', '.jsx', '.tsx'],
- },
- use: {
- loader: require.resolve('babel-loader'),
- options: {
- presets: [['@babel/preset-env', { targets: { node: '10' } }]],
- plugins: ['@babel/plugin-proposal-export-default-from', '@babel/plugin-proposal-class-properties'],
- },
- },
-}
-
-const swcTs = {
- test: /\.ts$/,
- use: {
- loader: require.resolve('swc-loader'),
- options: {
- jsc: {
- parser: {
- syntax: 'typescript',
- exportDefaultFrom: true,
- },
- },
- env: {
- targets: { node: '10' },
- },
- },
- },
-}
-
-const babelTs = {
- test: /\.ts$/,
- resolve: {
- extensions: ['.njs', '.js', '.nts', '.ts', '.jsx', '.tsx'],
- },
- use: {
- loader: require.resolve('babel-loader'),
- options: {
- presets: [['@babel/preset-env', { targets: { node: '10' } }], '@babel/preset-react'],
- plugins: ['@babel/plugin-transform-typescript'],
- },
- },
}
-const swcNullstackJavascript = {
- test: /\.(njs|nts|jsx|tsx)$/,
- use: {
- loader: require.resolve('swc-loader'),
- options: {
- jsc: {
- parser: {
- syntax: 'ecmascript',
- exportDefaultFrom: true,
- jsx: true,
- },
- transform: {
- react: {
- pragma: 'Nullstack.element',
- pragmaFrag: 'Nullstack.fragment',
- throwIfNamespace: true,
- },
- },
- },
- env: {
- targets: { node: '10' },
- },
- },
- },
-}
-
-const babelNullstackJavascript = {
- test: /\.(njs|jsx)$/,
- resolve: {
- extensions: ['.njs', '.js', '.nts', '.ts', '.jsx', '.tsx'],
- },
- use: {
- loader: require.resolve('babel-loader'),
- options: {
- presets: [['@babel/preset-env', { targets: { node: '10' } }], '@babel/preset-react'],
- plugins: [
- '@babel/plugin-proposal-export-default-from',
- '@babel/plugin-proposal-class-properties',
- [
- '@babel/plugin-transform-react-jsx',
- {
- pragma: 'Nullstack.element',
- pragmaFrag: 'Nullstack.fragment',
- throwIfNamespace: false,
- },
- ],
- ],
- },
- },
-}
-
-const swcNullstackTypescript = {
- test: /\.(nts|tsx)$/,
- use: {
- loader: require.resolve('swc-loader'),
- options: {
- jsc: {
- parser: {
- syntax: 'typescript',
- exportDefaultFrom: true,
- tsx: true,
- },
- transform: {
- react: {
- pragma: 'Nullstack.element',
- pragmaFrag: 'Nullstack.fragment',
- throwIfNamespace: true,
- },
- },
- },
- env: {
- targets: { node: '10' },
- },
- },
- },
-}
-
-const babelNullstackTypescript = {
- test: /\.(nts|tsx)$/,
- resolve: {
- extensions: ['.njs', '.js', '.nts', '.ts', '.jsx', '.tsx'],
- },
- use: {
- loader: require.resolve('babel-loader'),
- options: {
- presets: [['@babel/preset-env', { targets: { node: '10' } }], '@babel/preset-react'],
- plugins: [
- [
- '@babel/plugin-transform-typescript',
- {
- isTSX: true,
- allExtensions: true,
- tsxPragma: 'Nullstack.element',
- tsxPragmaFrag: 'Nullstack.fragment',
- },
- ],
- [
- '@babel/plugin-transform-react-jsx',
- {
- pragma: 'Nullstack.element',
- pragmaFrag: 'Nullstack.fragment',
- throwIfNamespace: false,
- },
- ],
- ],
- },
- },
-}
-
-function server(env, argv) {
- const dir = argv.input ? path.join(__dirname, argv.input) : process.cwd()
- const entryExtension = existsSync(path.join(dir, 'server.ts')) ? 'ts' : 'js'
- const icons = {}
- const publicFiles = readdirSync(path.join(dir, 'public'))
- const babel = argv.loader === 'babel'
- const iconFileRegex = /icon-(\d+)x\1\.[a-zA-Z]+/
- for (const file of publicFiles) {
- if (iconFileRegex.test(file)) {
- const size = file.split('x')[1].split('.')[0]
- icons[size] = `/${file}`
- }
- }
- const isDev = argv.environment === 'development'
- const folder = isDev ? '.development' : '.production'
- const devtool = isDev ? 'inline-cheap-module-source-map' : 'source-map'
- const minimize = !isDev
- const plugins = []
- if (isDev) {
- plugins.push(
- new NodemonPlugin({
- ext: '*',
- watch: ['.env', '.env.*', './.development/server.js'],
- script: './.development/server.js',
- nodeArgs: ['--enable-source-maps'],
- quiet: true,
- }),
- )
- }
+function config(platform, argv) {
+ const options = getOptions(platform, argv);
return {
- mode: argv.environment,
- infrastructureLogging: {
- console: customConsole,
- },
- entry: `./server.${entryExtension}`,
- output: {
- path: path.join(dir, folder),
- filename: 'server.js',
- chunkFilename: '[chunkhash].server.js',
- libraryTarget: 'umd',
- },
- resolve: {
- extensions: ['.njs', '.js', '.nts', '.ts', '.tsx', '.jsx'],
- },
- optimization: {
- minimize,
- minimizer: [
- new TerserPlugin({
- minify: terserMinimizer,
- // workaround: disable parallel to allow caching server
- parallel: argv.cache ? false : require('os').cpus().length - 1,
- }),
- ],
- },
- devtool,
- stats: 'errors-only',
- module: {
- rules: [
- {
- test: /nullstack.js$/,
- loader: getLoader('string-replace.js'),
- options: {
- multiple: [
- {
- search: /{{NULLSTACK_ENVIRONMENT_NAME}}/gi,
- replace: 'server',
- },
- ],
- },
- },
- {
- test: /environment.js$/,
- loader: getLoader('string-replace.js'),
- options: {
- multiple: [
- {
- search: /{{NULLSTACK_ENVIRONMENT_KEY}}/gi,
- replace: buildKey,
- },
- ],
- },
- },
- {
- test: /project.js$/,
- loader: getLoader('string-replace.js'),
- options: {
- multiple: [
- {
- search: /{{NULLSTACK_PROJECT_ICONS}}/gi,
- replace: JSON.stringify(icons),
- },
- ],
- },
- },
- babel ? babelJs : swcJs,
- babel ? babelTs : swcTs,
- babel ? babelNullstackJavascript : swcNullstackJavascript,
- {
- test: /\.(njs|nts|jsx|tsx)$/,
- loader: getLoader('register-static-from-server.js'),
- },
- {
- test: /\.s?[ac]ss$/,
- use: [{ loader: getLoader('ignore-import.js') }],
- },
- {
- test: /\.(njs|nts|jsx|tsx)$/,
- loader: getLoader('register-inner-components.js'),
- },
- {
- test: /\.(njs|nts|jsx|tsx)$/,
- loader: getLoader('inject-nullstack.js'),
- },
- babel ? babelNullstackTypescript : swcNullstackTypescript,
- {
- test: /\.(njs|nts|jsx|tsx)$/,
- loader: getLoader('add-source-to-node.js'),
- },
- {
- test: /\.(njs|nts|jsx|tsx)$/,
- loader: getLoader('transform-node-ref.js'),
- },
- {
- issuer: /worker.js/,
- resourceQuery: /raw/,
- type: 'asset/source',
- },
- ],
- },
- target: 'node',
- node: {
- __dirname: false,
- __filename: false,
- },
- plugins,
- cache: cacheFactory(argv, folder, 'server'),
+ mode: require('./webpack/mode')(options),
+ infrastructureLogging: require('./webpack/infrastructureLogging')(options),
+ entry: require('./webpack/entry')(options),
+ output: require('./webpack/output')(options),
+ resolve: require('./webpack/resolve')(options),
+ optimization: require('./webpack/optimization')(options),
+ devtool: require('./webpack/devtool')(options),
+ stats: require('./webpack/stats')(options),
+ target: require('./webpack/target')(options),
+ externals: require('./webpack/externals')(options),
+ node: require('./webpack/node')(options),
+ cache: require('./webpack/cache')(options),
+ module: require('./webpack/module')(options),
+ plugins: require('./webpack/plugins')(options),
+ experiments: require('./webpack/experiments')(options),
}
}
-function client(env, argv) {
- const disk = !!argv.disk
- const dir = argv.input ? path.join(__dirname, argv.input) : process.cwd()
- const entryExtension = existsSync(path.join(dir, 'client.ts')) ? 'ts' : 'js'
- const isDev = argv.environment === 'development'
- const folder = isDev ? '.development' : '.production'
- const devtool = isDev ? 'inline-cheap-module-source-map' : 'source-map'
- const minimize = !isDev
- const babel = argv.loader === 'babel'
- const plugins = [
- new MiniCssExtractPlugin({
- filename: 'client.css',
- chunkFilename: '[chunkhash].client.css',
- }),
- ]
- if (disk) {
- plugins.push(
- new CopyPlugin({
- patterns: [{ from: 'public', to: '../.development' }],
- }),
- )
- }
- return {
- infrastructureLogging: {
- console: customConsole,
- },
- mode: argv.environment,
- entry: `./client.${entryExtension}`,
- output: {
- publicPath: `/`,
- path: path.join(dir, folder),
- filename: 'client.js',
- chunkFilename: '[chunkhash].client.js',
- },
- resolve: {
- extensions: ['.njs', '.js', '.nts', '.ts', '.tsx', '.jsx'],
- },
- optimization: {
- minimize,
- minimizer: [
- new TerserPlugin({
- minify: terserMinimizer,
- }),
- ],
- },
- devtool,
- stats: 'errors-only',
- module: {
- rules: [
- {
- test: /client\.(js|ts)$/,
- loader: getLoader('inject-hmr.js'),
- },
- {
- test: /nullstack.js$/,
- loader: getLoader('string-replace.js'),
- options: {
- multiple: [
- {
- search: /{{NULLSTACK_ENVIRONMENT_NAME}}/gi,
- replace: 'client',
- },
- ],
- },
- },
- babel ? babelJs : swcJs,
- babel ? babelTs : swcTs,
- babel ? babelNullstackJavascript : swcNullstackJavascript,
- {
- test: /\.(njs|nts|jsx|tsx)$/,
- loader: getLoader('remove-import-from-client.js'),
- },
- {
- test: /\.(njs|nts|jsx|tsx)$/,
- loader: getLoader('remove-static-from-client.js'),
- },
- {
- test: /\.s?[ac]ss$/,
- use: [
- MiniCssExtractPlugin.loader,
- { loader: require.resolve('css-loader'), options: { url: false } },
- { loader: require.resolve('sass-loader'), options: { sassOptions: { fibers: false } } },
- ],
- },
- {
- test: /\.(njs|nts|jsx|tsx)$/,
- loader: getLoader('register-inner-components.js'),
- },
- {
- test: /\.(njs|nts|jsx|tsx)$/,
- loader: getLoader('inject-nullstack.js'),
- },
- babel ? babelNullstackTypescript : swcNullstackTypescript,
- {
- test: /\.(njs|nts|jsx|tsx)$/,
- loader: getLoader('add-source-to-node.js'),
- },
- {
- test: /\.(njs|nts|jsx|tsx)$/,
- loader: getLoader('transform-node-ref.js'),
- },
- ],
- },
- target: 'web',
- plugins,
- cache: cacheFactory(argv, folder, 'client'),
- }
+function server(_env, argv) {
+ return config('server', argv)
+}
+
+function client(_env, argv) {
+ return config('client', argv)
}
-module.exports = [server, client]
+module.exports = [server, client]
\ No newline at end of file
diff --git a/webpack/cache.js b/webpack/cache.js
new file mode 100644
index 00000000..2c0ac1f8
--- /dev/null
+++ b/webpack/cache.js
@@ -0,0 +1,14 @@
+const path = require('path')
+const { version } = require('../package.json')
+
+function cache(options) {
+ if (!options.cache) return
+ return {
+ type: 'filesystem',
+ cacheDirectory: path.posix.join(options.projectFolder, `.${options.environment}`, '.cache'),
+ name: options.target,
+ version,
+ }
+}
+
+module.exports = cache
\ No newline at end of file
diff --git a/webpack/devtool.js b/webpack/devtool.js
new file mode 100644
index 00000000..234071ce
--- /dev/null
+++ b/webpack/devtool.js
@@ -0,0 +1,9 @@
+function devtool(options) {
+ if (options.environment === 'development') {
+ return 'eval-cheap-module-source-map'
+ } else {
+ return 'hidden-source-map'
+ }
+}
+
+module.exports = devtool
\ No newline at end of file
diff --git a/webpack/entry.js b/webpack/entry.js
new file mode 100644
index 00000000..cfa6f024
--- /dev/null
+++ b/webpack/entry.js
@@ -0,0 +1,33 @@
+const path = require('path')
+
+function client(options) {
+ if (options.environment === 'production') {
+ return options.entry
+ }
+ return [
+ 'webpack-hot-middleware/client?log=false&path=/nullstack/hmr&noInfo=true&quiet=true&timeout=1000&reload=true',
+ path.posix.join(options.configFolder, 'shared', 'accept.js'),
+ options.entry
+ ]
+}
+
+function server(options) {
+ if (options.environment === 'production') {
+ return options.entry
+ }
+ return [
+ 'webpack/hot/poll?100',
+ path.posix.join(options.configFolder, 'shared', 'accept.js'),
+ options.entry
+ ]
+}
+
+function entry(options) {
+ if (options.target == 'client') {
+ return client(options)
+ } else {
+ return server(options)
+ }
+}
+
+module.exports = entry
\ No newline at end of file
diff --git a/webpack/experiments.js b/webpack/experiments.js
new file mode 100644
index 00000000..2c9c755f
--- /dev/null
+++ b/webpack/experiments.js
@@ -0,0 +1,12 @@
+function experiments(options) {
+ return // temporarily disabled
+ if (options.environment !== 'development') return
+ return {
+ lazyCompilation: {
+ entries: false,
+ imports: true
+ }
+ }
+}
+
+module.exports = experiments
\ No newline at end of file
diff --git a/webpack/externals.js b/webpack/externals.js
new file mode 100644
index 00000000..a7768153
--- /dev/null
+++ b/webpack/externals.js
@@ -0,0 +1,28 @@
+function client(options) {
+ if (options.environment === 'production') {
+ return {}
+ }
+ return {
+ 'webpack-hot-middleware/client':
+ 'webpack-hot-middleware/client?log=false&path=/nullstack/hmr&noInfo=true&quiet=true&timeout=1000&reload=true',
+ }
+}
+
+function server(options) {
+ if (options.environment === 'production') {
+ return {}
+ }
+ return {
+ 'webpack/hot/poll': 'webpack/hot/poll?100'
+ }
+}
+
+function externals(options) {
+ if (options.target == 'client') {
+ return client(options)
+ } else {
+ return server(options)
+ }
+}
+
+module.exports = externals
\ No newline at end of file
diff --git a/webpack/infrastructureLogging.js b/webpack/infrastructureLogging.js
new file mode 100644
index 00000000..002dc634
--- /dev/null
+++ b/webpack/infrastructureLogging.js
@@ -0,0 +1,7 @@
+function infrastructureLogging(_) {
+ return {
+ level: 'error'
+ }
+}
+
+module.exports = infrastructureLogging
\ No newline at end of file
diff --git a/webpack/mode.js b/webpack/mode.js
new file mode 100644
index 00000000..ec71f9df
--- /dev/null
+++ b/webpack/mode.js
@@ -0,0 +1,5 @@
+function mode(options) {
+ return options.environment
+}
+
+module.exports = mode
\ No newline at end of file
diff --git a/webpack/module.js b/webpack/module.js
new file mode 100644
index 00000000..b7e5b018
--- /dev/null
+++ b/webpack/module.js
@@ -0,0 +1,194 @@
+const path = require('path')
+const { readdirSync } = require('fs')
+
+function icons(options) {
+ const icons = {}
+ const publicFiles = readdirSync(path.posix.join(options.projectFolder, 'public'))
+ const iconFileRegex = /icon-(\d+)x\1\.[a-zA-Z]+/
+ for (const file of publicFiles) {
+ if (iconFileRegex.test(file)) {
+ const size = file.split('x')[1].split('.')[0]
+ icons[size] = `/${file}`
+ }
+ }
+ return { ICONS: JSON.stringify(icons) }
+}
+
+function environment(_options) {
+ const crypto = require('crypto')
+ const key = crypto.randomBytes(20).toString('hex')
+ return { KEY: `"${key}"` }
+}
+
+function scss(options) {
+ if (options.target !== 'client') return
+
+ return {
+ test: /\.s[ac]ss$/,
+ use: [
+ {
+ loader: require.resolve('sass-loader'),
+ options: { sassOptions: { fibers: false } }
+ }
+ ],
+ }
+}
+
+function css(options) {
+ if (options.target !== 'client') return
+
+ const { loader } = require('mini-css-extract-plugin')
+ return {
+ test: /\.s?[ac]ss$/,
+ use: [
+ loader,
+ {
+ loader: require.resolve('css-loader'),
+ options: { url: false }
+ }
+ ],
+ }
+}
+
+function swc(options, other) {
+ const config = {
+ test: other.test,
+ use: {
+ loader: require.resolve('swc-loader'),
+ options: { jsc: {}, env: {} }
+ }
+ }
+
+ config.use.options.jsc.experimental = {
+ cacheRoot: path.posix.join(options.projectFolder, options.buildFolder, '.cache', '.swc'),
+ plugins: [
+ [
+ require.resolve('swc-plugin-nullstack'),
+ {
+ development: options.environment === 'development',
+ client: options.target === 'client',
+ template: !!other.template
+ }
+ ]
+ ]
+ }
+
+ if (options.target === 'server') {
+ config.use.options.env.targets = { node: process.versions.node }
+ } else {
+ config.use.options.env.targets = 'defaults'
+ }
+
+ config.use.options.jsc.parser = {
+ syntax: other.syntax,
+ exportDefaultFrom: true,
+ }
+
+ if (other.template) {
+ config.use.options.jsc.parser[other.template] = true
+ }
+
+ config.use.options.jsc.transform = {
+ useDefineForClassFields: false,
+ react: {
+ pragma: '$runtime.element',
+ pragmaFrag: '$runtime.fragment',
+ runtime: 'classic',
+ throwIfNamespace: true,
+ },
+ }
+
+ if (options.target === 'server' && !other.template) {
+ config.use.options.jsc.transform.constModules = {
+ globals: {
+ "nullstack/environment": environment(options),
+ "nullstack/project": icons(options),
+ }
+ }
+ }
+
+ return config
+}
+
+function js(options) {
+ return swc(options, {
+ test: /\.js$/,
+ syntax: 'ecmascript',
+ })
+}
+
+function ts(options) {
+ return swc(options, {
+ test: /\.ts$/,
+ syntax: 'typescript',
+ })
+}
+
+function njs(options) {
+ return swc(options, {
+ test: /\.(njs|jsx)$/,
+ syntax: 'ecmascript',
+ template: 'jsx',
+ })
+}
+
+function nts(options) {
+ return swc(options, {
+ test: /\.(nts|tsx)$/,
+ syntax: 'typescript',
+ template: 'tsx',
+ })
+}
+
+function shutUp(options) {
+ return {
+ test: /node_modules[\\/](webpack[\\/]hot|webpack-hot-middleware|mini-css-extract-plugin)/,
+ loader: path.posix.join(options.configFolder, 'loaders', 'shut-up-loader.js'),
+ }
+}
+
+function raw() {
+ return {
+ issuer: /worker.js/,
+ resourceQuery: /raw/,
+ type: 'asset/source',
+ }
+}
+
+function runtime(options) {
+ return {
+ test: /\.(nts|tsx|njs|jsx)$/,
+ loader: path.posix.join(options.configFolder, 'loaders', 'inject-runtime.js'),
+ }
+}
+
+function trace(options) {
+ if (!options.trace) return
+ process.env.__NULLSTACK_TARGET = options.target
+ return {
+ loader: path.posix.join(options.configFolder, 'loaders', 'trace.js'),
+ }
+}
+
+function rules(options) {
+ return [
+ trace(options),
+ css(options),
+ scss(options),
+ js(options),
+ ts(options),
+ njs(options),
+ nts(options),
+ shutUp(options),
+ raw(options),
+ runtime(options)
+ ].filter(Boolean)
+}
+
+function iWishModuleWasntAReservedWord(options) {
+ return {
+ rules: rules(options)
+ }
+}
+
+module.exports = iWishModuleWasntAReservedWord
\ No newline at end of file
diff --git a/webpack/node.js b/webpack/node.js
new file mode 100644
index 00000000..013316cf
--- /dev/null
+++ b/webpack/node.js
@@ -0,0 +1,8 @@
+function node(_) {
+ return {
+ __dirname: false,
+ __filename: false,
+ }
+}
+
+module.exports = node
\ No newline at end of file
diff --git a/webpack/optimization.js b/webpack/optimization.js
new file mode 100644
index 00000000..6889773e
--- /dev/null
+++ b/webpack/optimization.js
@@ -0,0 +1,31 @@
+function js() {
+ const TerserPlugin = require('terser-webpack-plugin')
+ return new TerserPlugin({
+ minify: TerserPlugin.swcMinify,
+ terserOptions: {
+ mangle: false,
+ compress: {
+ unused: false,
+ },
+ keepFnames: true,
+ sourceMap: true,
+ }
+ })
+}
+
+function css(options) {
+ if (options.target !== 'client') return false
+ const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
+ return new CssMinimizerPlugin({
+ minify: CssMinimizerPlugin.lightningCssMinify,
+ })
+}
+
+function optimization(options) {
+ return {
+ minimize: options.environment === 'production',
+ minimizer: [js(options), css(options)].filter(Boolean),
+ }
+}
+
+module.exports = optimization
\ No newline at end of file
diff --git a/webpack/output.js b/webpack/output.js
new file mode 100644
index 00000000..b5ee5d24
--- /dev/null
+++ b/webpack/output.js
@@ -0,0 +1,22 @@
+const path = require('path')
+
+function output(options) {
+ return {
+ publicPath: `/`,
+ path: path.posix.join(options.projectFolder, options.buildFolder),
+ filename: `${options.target}.js`,
+ chunkFilename: `[chunkhash].${options.target}.js`,
+ hotUpdateChunkFilename: `nullstack-${options.target}-update-[id]-[fullhash].js`,
+ hotUpdateMainFilename: `nullstack-${options.target}-update-[runtime]-[fullhash].json`,
+ pathinfo: false,
+ libraryTarget: 'umd',
+ clean: false,
+ // {
+ // keep(asset) {
+ // return asset.startsWith('.') || !asset.includes(options.target)
+ // },
+ // }
+ }
+}
+
+module.exports = output
\ No newline at end of file
diff --git a/webpack/plugins.js b/webpack/plugins.js
new file mode 100644
index 00000000..f3f7fbe8
--- /dev/null
+++ b/webpack/plugins.js
@@ -0,0 +1,53 @@
+function css(options) {
+ if (options.target !== 'client') return false
+
+ const MiniCssExtractPlugin = require('mini-css-extract-plugin')
+ return new MiniCssExtractPlugin({
+ filename: `${options.target}.css`,
+ chunkFilename: `[chunkhash].${options.target}.css`,
+ })
+}
+
+function hmr(options) {
+ if (options.environment !== 'development') return false
+
+ const { HotModuleReplacementPlugin } = require('webpack')
+ return new HotModuleReplacementPlugin()
+}
+
+function copy(options) {
+ if (!options.disk) return false
+
+ const CopyPlugin = require('copy-webpack-plugin')
+ return new CopyPlugin({
+ patterns: [
+ { from: 'public', to: `../${options.buildFolder}` }
+ ],
+ })
+}
+
+function nodemon(options) {
+ if (options.environment !== 'development') return false
+ if (options.target !== 'server') return false
+
+ const NodemonPlugin = require('nodemon-webpack-plugin')
+ const dotenv = options.name ? `.env.${options.name}` : '.env'
+ return new NodemonPlugin({
+ ext: '*',
+ watch: [dotenv, './server.js'],
+ script: './.development/server.js',
+ nodeArgs: ['--enable-source-maps'],
+ quiet: true,
+ })
+}
+
+function plugins(options) {
+ return [
+ css(options),
+ hmr(options),
+ copy(options),
+ nodemon(options),
+ ].filter(Boolean)
+}
+
+module.exports = plugins
\ No newline at end of file
diff --git a/webpack/resolve.js b/webpack/resolve.js
new file mode 100644
index 00000000..a65e6f10
--- /dev/null
+++ b/webpack/resolve.js
@@ -0,0 +1,12 @@
+const path = require('path')
+
+function resolve(options) {
+ return {
+ extensions: ['.njs', '.js', '.nts', '.ts', '.tsx', '.jsx'],
+ alias: {
+ nullstack: path.join(options.configFolder, options.target),
+ }
+ }
+}
+
+module.exports = resolve
\ No newline at end of file
diff --git a/webpack/stats.js b/webpack/stats.js
new file mode 100644
index 00000000..1189d8a6
--- /dev/null
+++ b/webpack/stats.js
@@ -0,0 +1,5 @@
+function stats(_) {
+ return 'none'
+}
+
+module.exports = stats
\ No newline at end of file
diff --git a/webpack/target.js b/webpack/target.js
new file mode 100644
index 00000000..cff58f9e
--- /dev/null
+++ b/webpack/target.js
@@ -0,0 +1,5 @@
+function target(options) {
+ return options.target == 'client' ? 'web' : 'node'
+}
+
+module.exports = target
\ No newline at end of file
diff --git a/workers/dynamicInstall.js b/workers/dynamicInstall.js
index f9f088eb..7dcead3f 100644
--- a/workers/dynamicInstall.js
+++ b/workers/dynamicInstall.js
@@ -5,7 +5,7 @@ function install(event) {
'/manifest.webmanifest',
`/client.js?fingerprint=${self.context.environment.key}`,
`/client.css?fingerprint=${self.context.environment.key}`,
- '{{BUNDLE}}',
+ globalThis.__NULLSTACK_BUNDLE,
]
event.waitUntil(
(async function () {
diff --git a/workers/staticInstall.js b/workers/staticInstall.js
index 7c128077..f18c8774 100644
--- a/workers/staticInstall.js
+++ b/workers/staticInstall.js
@@ -5,7 +5,7 @@ function install(event) {
'/manifest.webmanifest',
`/client.js?fingerprint=${self.context.environment.key}`,
`/client.css?fingerprint=${self.context.environment.key}`,
- '{{BUNDLE}}',
+ globalThis.__NULLSTACK_BUNDLE,
`/nullstack/${self.context.environment.key}/offline/index.html`,
].flat()
event.waitUntil(