diff --git a/client/index.js b/client/index.js index 542907e3..1f4862b3 100644 --- a/client/index.js +++ b/client/index.js @@ -108,7 +108,7 @@ export default class Nullstack { } if (module.hot) { - const socket = new WebSocket('ws' + window.location.origin.slice(4) + '/ws'); + const socket = new WebSocket('ws' + router.base.slice(4) + '/ws'); window.lastHash socket.onmessage = async function (e) { const data = JSON.parse(e.data) diff --git a/client/ref.js b/client/ref.js index a9d2b829..5cb9775f 100644 --- a/client/ref.js +++ b/client/ref.js @@ -5,7 +5,7 @@ function setup(attributes, element) { const property = attributes.ref.property if (typeof object[property] === 'function') { setTimeout(() => { - object[property]({ element }) + object[property]({ ...attributes, element }) }, 0) } else { object[property] = element diff --git a/client/router.js b/client/router.js index b9f5a82b..28e2415d 100644 --- a/client/router.js +++ b/client/router.js @@ -90,7 +90,7 @@ class Router { get base() { if (this._base) return this._base - this._base = window.location.origin + this._base = new URL(document.querySelector('[rel="canonical"]').href).origin return this._base } diff --git a/package.json b/package.json index 0357da06..f1006209 100644 --- a/package.json +++ b/package.json @@ -12,11 +12,12 @@ }, "types": "./types/index.d.ts", "dependencies": { - "@swc/core": "1.2.179", "@babel/parser": "7.17.12", "@babel/traverse": "7.17.12", + "@swc/core": "1.2.179", "body-parser": "1.20.0", "commander": "8.3.0", + "copy-webpack-plugin": "^11.0.0", "cors": "2.8.5", "css-loader": "6.7.1", "dotenv": "8.6.0", @@ -33,4 +34,4 @@ "webpack-dev-server": "4.9.0", "ws": "7.5.7" } -} \ No newline at end of file +} diff --git a/scripts/index.js b/scripts/index.js index 77c8afdc..7b9cbe28 100755 --- a/scripts/index.js +++ b/scripts/index.js @@ -17,10 +17,6 @@ function getCompiler(options) { return webpack(getConfig(options)) } -function getServerCompiler(options) { - return webpack(getConfig(options)[0]) -} - function loadEnv(env) { let envPath = '.env' if (env) { @@ -64,22 +60,27 @@ async function start({ input, port, env, mode = 'spa', hot }) { process.env['NULSTACK_SERVER_PORT_YOU_SHOULD_NOT_CARE_ABOUT'] = ports[0] process.env['NULSTACK_SERVER_SOCKET_PORT_YOU_SHOULD_NOT_CARE_ABOUT'] = ports[1] process.env['NULLSTACK_ENVIRONMENT_HOT'] = (!!hot).toString() + process.env['NULLSTACK_PROJECT_DOMAIN'] ??= 'localhost' + process.env['NULLSTACK_WORKER_PROTOCOL'] ??= 'http' const devServerOptions = { hot: 'only', open: false, + host: process.env['NULLSTACK_PROJECT_DOMAIN'], devMiddleware: { index: false, - stats: 'none' + stats: 'none', + writeToDisk: true, }, client: { overlay: { errors: true, warnings: false }, logging: 'none', progress: false, - reconnect: true + reconnect: true, + webSocketURL: `${process.env['NULLSTACK_WORKER_PROTOCOL'].replace('http', 'ws')}://${process.env['NULLSTACK_PROJECT_DOMAIN']}:${process.env['NULLSTACK_SERVER_PORT']}/ws` }, proxy: { context: () => true, - target: `http://localhost:${process.env['NULSTACK_SERVER_PORT_YOU_SHOULD_NOT_CARE_ABOUT']}`, + target: `${process.env['NULLSTACK_WORKER_PROTOCOL']}://${process.env['NULLSTACK_PROJECT_DOMAIN']}:${process.env['NULSTACK_SERVER_PORT_YOU_SHOULD_NOT_CARE_ABOUT']}`, proxyTimeout: 10 * 60 * 1000, timeout: 10 * 60 * 1000, }, @@ -104,19 +105,12 @@ async function start({ input, port, env, mode = 'spa', hot }) { }; const clientCompiler = getCompiler({ environment, input }); const server = new WebpackDevServer(devServerOptions, clientCompiler); - const serverCompiler = getServerCompiler({ environment, input }); - let once = false - serverCompiler.watch({}, (error, stats) => { - if (!once) { - server.startCallback(() => { - console.log('\x1b[36m%s\x1b[0m', ` ✅️ Your application is ready at http://localhost:${process.env['NULLSTACK_SERVER_PORT']}\n`); - }); - once = true - } - if (stats.hasErrors()) { - console.log(stats.toString({ colors: true })) - } - }); + const portChecker = require('express')().listen(process.env['NULLSTACK_SERVER_PORT'], () => { + portChecker.close() + server.startCallback(() => { + console.log('\x1b[36m%s\x1b[0m', ` ✅️ Your application is ready at http://${process.env['NULLSTACK_PROJECT_DOMAIN']}:${process.env['NULLSTACK_SERVER_PORT']}\n`); + }); + }) } function build({ input, output, cache, env, mode = 'ssr' }) { @@ -143,7 +137,6 @@ program .addOption(new program.Option('-m, --mode ', 'Build production bundles').choices(['ssr', 'spa'])) .option('-p, --port ', 'Port number to run the server') .option('-i, --input ', 'Path to project that will be started') - .option('-o, --output ', 'Path to build output folder') .option('-e, --env ', 'Name of the environment file that should be loaded') .option('--hot', 'Enable hot module replacement') .helpOption('-h, --help', 'Learn more about this command') diff --git a/server/environment.js b/server/environment.js index 1345ea2f..7f74e2b4 100644 --- a/server/environment.js +++ b/server/environment.js @@ -8,7 +8,10 @@ environment.mode = process.env.NULLSTACK_ENVIRONMENT_MODE || 'ssr'; environment.key = "{{NULLSTACK_ENVIRONMENT_KEY}}" environment.name = process.env.NULLSTACK_ENVIRONMENT_NAME || ''; -environment.hot = process.env.NULLSTACK_ENVIRONMENT_HOT === 'true' + +if (environment.development) { + environment.hot = process.env.NULLSTACK_ENVIRONMENT_HOT === 'true' +} Object.freeze(environment); diff --git a/server/project.js b/server/project.js index 27d4e693..83915cbf 100644 --- a/server/project.js +++ b/server/project.js @@ -1,6 +1,6 @@ import environment from './environment'; import worker from './worker'; -import server from './server'; +import reqres from './reqres'; const project = {}; @@ -19,9 +19,18 @@ project.favicon = '/favicon-96x96.png'; project.disallow = []; project.icons = JSON.parse(`{{NULLSTACK_PROJECT_ICONS}}`); +function getHost() { + if (reqres.request?.headers?.host) { + return reqres.request.headers.host + } + if (project.domain === 'localhost') { + return `localhost:${process.env['NULLSTACK_SERVER_PORT']}` + } + return project.domain +} + export function generateBase() { - const port = project.domain === 'localhost' ? `:${server.port}` : ''; - return `${worker.protocol}://${project.domain}${port}`; + return `${worker.protocol}://${getHost()}`; } export default project; \ No newline at end of file diff --git a/server/server.js b/server/server.js index 903a3547..cee880dc 100644 --- a/server/server.js +++ b/server/server.js @@ -17,6 +17,7 @@ import template from './template'; import { generateServiceWorker } from './worker'; import reqres from './reqres' import WebSocket from 'ws'; +import { writeFileSync } from 'fs' if (!global.fetch) { global.fetch = fetch; @@ -204,7 +205,12 @@ server.start = function () { if (!response.headersSent) { const status = scope.context.page.status; const html = template(scope); + reqres.request = null + reqres.response = null response.status(status).send(html); + } else { + reqres.request = null + reqres.response = null } }); @@ -214,12 +220,17 @@ server.start = function () { process.exit(); } - server.listen(server.port, () => { + server.listen(server.port, async () => { if (environment.development) { + const content = await server.prerender('/'); + const target = process.cwd() + `/.development/index.html` + writeFileSync(target, content) const socket = new WebSocket(`ws://localhost:${process.env['NULLSTACK_SERVER_PORT']}/ws`); socket.onopen = async function (e) { socket.send('{"type":"NULLSTACK_SERVER_STARTED"}') } + } else { + console.log('\x1b[36m%s\x1b[0m', ` ✅️ Your application is ready at http://${process.env['NULLSTACK_PROJECT_DOMAIN']}:${process.env['NULLSTACK_SERVER_PORT']}\n`); } }); } diff --git a/tests/package.json b/tests/package.json index e6cbb192..422014c6 100644 --- a/tests/package.json +++ b/tests/package.json @@ -15,7 +15,7 @@ "scripts": { "start": "npx nullstack start --input=./tests --port=6969 --env=test --mode=spa --hot", "build": "npx nullstack build --input=./tests --env=test", - "test": "npm run build && jest", + "test": "npm run build && jest --runInBand", "script": "node src/scripts/run.js" } } \ No newline at end of file diff --git a/tests/src/Context.njs b/tests/src/Context.njs index f22e0964..ca5947a7 100644 --- a/tests/src/Context.njs +++ b/tests/src/Context.njs @@ -8,26 +8,57 @@ class Context extends Nullstack { context.framework = 'Nullstack'; } - static async getContextKey({framework}) { + static async getContextKey({ framework }) { return framework; } + static staticFunction(context) { + return context === undefined + } + + static _staticUnderlineFunction(context) { + return context === undefined + } + + static async _staticAsyncUnderlineFunction(context) { + return context === undefined + } + + static async invokeStaticAsyncUnderlineFunction() { + return await this._staticAsyncUnderlineFunction() + } + async initiate(context) { await this.setContextKey(); context.framework = await this.getContextKey(); this.setFrameworkInitial(); + this.staticFunctionHasNoContext = await Context.staticFunction() + this.staticUnderlineFunctionHasNoContext = await Context._staticUnderlineFunction() + this.staticAsyncUnderlineFunctionHasNoContext = await Context.invokeStaticAsyncUnderlineFunction() } - setFrameworkInitial({framework}) { + async hydrate() { + this.hydratedStaticFunctionHasNoContext = await Context.staticFunction() + this.hydratedStaticUnderlineFunctionHasNoContext = await Context._staticUnderlineFunction() + this.hydratedStaticAsyncUnderlineFunctionHasNoContext = await Context.invokeStaticAsyncUnderlineFunction() + } + + setFrameworkInitial({ framework }) { this.frameworkInitial = framework[0]; } - - render({framework}) { + + render({ framework }) { return ( -
-
-
-
+
) } diff --git a/tests/src/Context.test.js b/tests/src/Context.test.js index 7d015e5e..e7dc3000 100644 --- a/tests/src/Context.test.js +++ b/tests/src/Context.test.js @@ -29,4 +29,40 @@ describe('Context', () => { expect(element).toBeTruthy(); }); + test('hydrated static async underline function has no context', async () => { + await page.waitForSelector('[data-hydrated-static-async-underline-function-has-no-context]'); + const element = await page.$('[data-hydrated-static-async-underline-function-has-no-context]'); + expect(element).toBeTruthy(); + }); + + test('hydrated static underline function has no context', async () => { + await page.waitForSelector('[data-hydrated-static-underline-function-has-no-context]'); + const element = await page.$('[data-hydrated-static-underline-function-has-no-context]'); + expect(element).toBeTruthy(); + }); + + test('hydrated static function has no context', async () => { + await page.waitForSelector('[data-hydrated-static-function-has-no-context]'); + const element = await page.$('[data-hydrated-static-function-has-no-context]'); + expect(element).toBeTruthy(); + }); + + test('static async underline function has no context', async () => { + await page.waitForSelector('[data-static-async-underline-function-has-no-context]'); + const element = await page.$('[data-static-async-underline-function-has-no-context]'); + expect(element).toBeTruthy(); + }); + + test('static underline function has no context', async () => { + await page.waitForSelector('[data-static-underline-function-has-no-context]'); + const element = await page.$('[data-static-underline-function-has-no-context]'); + expect(element).toBeTruthy(); + }); + + test('static function has no context', async () => { + await page.waitForSelector('[data-static-function-has-no-context]'); + const element = await page.$('[data-static-function-has-no-context]'); + expect(element).toBeTruthy(); + }); + }); \ No newline at end of file diff --git a/tests/src/Refs.njs b/tests/src/Refs.njs index ea08a5af..35a59b48 100644 --- a/tests/src/Refs.njs +++ b/tests/src/Refs.njs @@ -8,7 +8,8 @@ class Refs extends Nullstack { this.id = this._element.id } - setRef({ element, refInstanceCount }) { + setRef({ element, refInstanceCount, id }) { + this.refReceivedProps = element.id === id this._function = element this.isOnDOM = element.offsetHeight > 0 && refInstanceCount } @@ -29,7 +30,7 @@ class Refs extends Nullstack { - span + span
diff --git a/tests/src/Refs.test.js b/tests/src/Refs.test.js index 71a38e1a..b0069952 100644 --- a/tests/src/Refs.test.js +++ b/tests/src/Refs.test.js @@ -35,6 +35,13 @@ describe('Refs', () => { expect(element).toBeTruthy(); }); + test('refs functions receive attributes as argument', async () => { + await page.waitForSelector('[data-ref-received-props]'); + const element = await page.$('[data-ref-received-props]'); + expect(element).toBeTruthy(); + }); + + test('refs functions only run after the element is appended do DOM', async () => { await page.waitForSelector('[data-dom="0"]'); const element = await page.$('[data-dom="0"]'); diff --git a/tests/src/ServerFunctions.njs b/tests/src/ServerFunctions.njs index 02e84063..325817a5 100644 --- a/tests/src/ServerFunctions.njs +++ b/tests/src/ServerFunctions.njs @@ -84,7 +84,7 @@ class ServerFunctions extends Nullstack { render() { return ( -
+
diff --git a/tests/src/ServerFunctions.test.js b/tests/src/ServerFunctions.test.js index 85e0a38f..4e40f30c 100644 --- a/tests/src/ServerFunctions.test.js +++ b/tests/src/ServerFunctions.test.js @@ -1,10 +1,12 @@ -beforeAll(async () => { - await page.goto('http://localhost:6969/server-functions'); -}); - describe('ServerFunctions', () => { + beforeEach(async () => { + await page.goto('http://localhost:6969/server-functions'); + }); + + test('instance can use returned values', async () => { + await page.waitForSelector('[data-hydrated]') await page.click('.set-count-to-one'); await page.waitForSelector('[data-count="1"]'); const element = await page.$('[data-count="1"]'); @@ -12,6 +14,7 @@ describe('ServerFunctions', () => { }); test('server functions accept an object as argument', async () => { + await page.waitForSelector('[data-hydrated]') await page.click('.set-count-to-two'); await page.waitForSelector('[data-count="2"]'); const element = await page.$('[data-count="2"]'); @@ -19,6 +22,7 @@ describe('ServerFunctions', () => { }); test('server functions serialize and deserialize dates', async () => { + await page.waitForSelector('[data-hydrated]') await page.click('.set-date'); await page.waitForSelector('[data-year="1992"]'); const element = await page.$('[data-year="1992"]'); diff --git a/webpack.config.js b/webpack.config.js index bc75876c..55a90dbf 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -4,6 +4,7 @@ const TerserPlugin = require('terser-webpack-plugin'); const crypto = require("crypto"); const { readdirSync } = require('fs'); const NodemonPlugin = require('nodemon-webpack-plugin'); +const CopyPlugin = require("copy-webpack-plugin"); const buildKey = crypto.randomBytes(20).toString('hex'); @@ -122,6 +123,15 @@ function server(env, argv) { const devtool = isDev ? 'inline-cheap-module-source-map' : false; 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 + })) + } return { mode: argv.environment, infrastructureLogging: { @@ -246,12 +256,10 @@ function client(env, argv) { }), ] if (isDev) { - plugins.push(new NodemonPlugin({ - ext: '*', - watch: [".env", ".env.*", './.development/*.*'], - script: './.development/server.js', - nodeArgs: ['--enable-source-maps'], - quiet: true + plugins.push(new CopyPlugin({ + patterns: [ + { from: "public", to: "../.development" }, + ] })) } return {