Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion client/ref.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion client/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -33,4 +34,4 @@
"webpack-dev-server": "4.9.0",
"ws": "7.5.7"
}
}
}
35 changes: 14 additions & 21 deletions scripts/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
},
Expand All @@ -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' }) {
Expand All @@ -143,7 +137,6 @@ program
.addOption(new program.Option('-m, --mode <mode>', 'Build production bundles').choices(['ssr', 'spa']))
.option('-p, --port <port>', 'Port number to run the server')
.option('-i, --input <input>', 'Path to project that will be started')
.option('-o, --output <output>', 'Path to build output folder')
.option('-e, --env <name>', 'Name of the environment file that should be loaded')
.option('--hot', 'Enable hot module replacement')
.helpOption('-h, --help', 'Learn more about this command')
Expand Down
5 changes: 4 additions & 1 deletion server/environment.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
15 changes: 12 additions & 3 deletions server/project.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import environment from './environment';
import worker from './worker';
import server from './server';
import reqres from './reqres';

const project = {};

Expand All @@ -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;
13 changes: 12 additions & 1 deletion server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
}
});

Expand All @@ -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`);
}
});
}
Expand Down
2 changes: 1 addition & 1 deletion tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
47 changes: 39 additions & 8 deletions tests/src/Context.njs
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div>
<div data-framework={framework} />
<div data-framework-initial={this.frameworkInitial} />
</div>
<div
data-framework={framework}
data-framework-initial={this.frameworkInitial}
data-static-function-has-no-context={this.staticFunctionHasNoContext}
data-static-underline-function-has-no-context={this.staticUnderlineFunctionHasNoContext}
data-static-async-underline-function-has-no-context={this.staticAsyncUnderlineFunctionHasNoContext}
data-hydrated-static-function-has-no-context={this.hydratedStaticFunctionHasNoContext}
data-hydrated-static-underline-function-has-no-context={this.hydratedStaticUnderlineFunctionHasNoContext}
data-hydrated-static-async-underline-function-has-no-context={this.hydratedStaticAsyncUnderlineFunctionHasNoContext}
/>
)
}

Expand Down
36 changes: 36 additions & 0 deletions tests/src/Context.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});

});
5 changes: 3 additions & 2 deletions tests/src/Refs.njs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -29,7 +30,7 @@ class Refs extends Nullstack {
<span id="composed-computed" ref={this[this.composedComputed]} data-id={this._composedComputed?.id} />
<span id="logical-computed" ref={this[['_logical', 'Computed'].join('')]} data-id={this._logicalComputed?.id} />
<span id="literal-computed" ref={this['_literalComputed']} data-id={this._literalComputed?.id} />
<span id="function" ref={this.setRef} data-dom={this.isOnDOM} data-id={this._function?.id}>span</span>
<span id="function" ref={this.setRef} data-ref-received-props={this.refReceivedProps} data-dom={this.isOnDOM} data-id={this._function?.id}>span</span>
<Bubble ref={this._bubble} />
<button onclick={this.changeInstance}>Change Instance</button>
</div>
Expand Down
7 changes: 7 additions & 0 deletions tests/src/Refs.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"]');
Expand Down
2 changes: 1 addition & 1 deletion tests/src/ServerFunctions.njs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ class ServerFunctions extends Nullstack {

render() {
return (
<div>
<div data-hydrated={this.hydrated}>
<button class="set-count-to-one" onclick={this.setCountToOne}>1</button>
<button class="set-count-to-two" onclick={this.setCountToTwo}>2</button>
<button class="set-date" onclick={this.setDate}>1992</button>
Expand Down
12 changes: 8 additions & 4 deletions tests/src/ServerFunctions.test.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,28 @@
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"]');
expect(element).toBeTruthy();
});

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"]');
expect(element).toBeTruthy();
});

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"]');
Expand Down
Loading