@changke/staticnext-lib
v2.0.0
Published
Reusable library code for StaticNext (forked & typed)
Readme
@changke/staticnext-lib
A zero-dependency TypeScript library providing browser-side utility services for the StaticNext framework. Includes AJAX, PubSub, DOM helpers, cookie management, lazy loading, logging, and URL utilities.
- Zero runtime dependencies
- ESM-only (
"type": "module") - Targets modern browsers (ES2023, real DOM APIs)
- Built with plain
tsc(no bundler)
Installation
npm install @changke/staticnext-libRequires Node >= 22 for development tooling. The library itself runs in the browser.
Quick Start
import {dom, pubSub, ajaxP, logger} from '@changke/staticnext-lib';
// DOM class manipulation (chainable)
dom.addClass(document.body, 'loaded').removeClass(document.body, 'loading');
// Pub/Sub events
const sub = pubSub.subscribe('user:login', data => {
Logger.log('App', `User logged in: ${data}`);
});
pubSub.publish('user:login', {id: 42});
sub.remove();
// Fetch JSON
const users = await ajaxP.getJSON<User[]>('/api/users', {page: '1'});API Reference
All services (except Module, autoInit, and getUrlPath) are exported as
singleton instances -- import them directly and call methods on them.
AjaxP
Promise-based fetch wrapper with JSON support.
import {ajaxP} from '@changke/staticnext-lib';
// GET with query params
const data = await ajaxP.getJSON<MyType>('/api/items', {page: '1', limit: '10'});
// POST JSON
const result = await ajaxP.postJSON<InputType, ResponseType>('/api/items', {name: 'foo'});
// PUT JSON
await ajaxP.putJSON('/api/items/1', {name: 'updated'});
// DELETE
await ajaxP.deleteRequest('/api/items/1', {'X-Token': 'abc'});
// URL utilities
ajaxP.toQueryString({a: '1', b: '2'}); // => 'a=1&b=2'
ajaxP.buildFullUrl('/api?foo=bar', {a: '1'}); // => '/api?foo=bar&a=1'Methods:
| Method | Signature | Description |
|-----------------|---------------------------------------------|------------------------------------|
| getJSON | <T>(url, data?, opts?) => Promise<T> | GET request, returns parsed JSON |
| postJSON | <T, U>(url, data, headers?) => Promise<U> | POST with JSON body |
| putJSON | <T, U>(url, data, headers?) => Promise<U> | PUT with JSON body |
| deleteRequest | <T>(url, headers) => Promise<T> | DELETE request |
| toQueryString | (obj?) => string | Convert object to URL query string |
| buildFullUrl | (url, data?) => string | Append query params to a URL |
Errors throw a ResponseError (extends Error) with a .response property containing the
raw Response object:
import type {ResponseError} from '@changke/staticnext-lib/dist/services/ajaxp.js';PubSub
Simple publish/subscribe event hub.
import {pubSub} from '@changke/staticnext-lib';
// Subscribe (returns handle with .remove())
const sub = pubSub.subscribe('topic', data => {
console.log(data);
});
// Publish
pubSub.publish('topic', {key: 'value'});
// Unsubscribe
sub.remove();
// One-time subscription
pubSub.subscribeOnce('init', data => {
console.log('called only once');
});Methods:
| Method | Signature | Description |
|-----------------|-----------------------------------|------------------------------------------------------|
| subscribe | (topic, listener) => Subscribed | Subscribe to a topic; returns {remove: () => void} |
| publish | (topic, data?) => void | Publish data to all topic listeners |
| subscribeOnce | (topic, listener) => void | Subscribe for a single event only |
Types:
import type {ListenerFn, Subscribed} from '@changke/staticnext-lib/dist/services/pubsub.js';
type ListenerFn = (data: unknown) => void;
interface Subscribed { remove: () => void; }DOM
Utility functions for common DOM tasks. Methods that modify elements return this for
chaining.
import {dom} from '@changke/staticnext-lib';
// Class manipulation (chainable)
dom.addClass(element, 'active').removeClass(element, 'hidden');
// Works with single elements or NodeList/HTMLCollection
dom.addClass(document.querySelectorAll('.item'), 'highlight');
// Check class
dom.hasClass(element, 'active'); // => true
// Remove all children
dom.removeAllChildren(container);
// Create DocumentFragment from HTML string
const fragment = dom.fromHtml('<p>Hello <strong>world</strong></p>');
document.body.appendChild(fragment);
// Iterate over NodeList
dom.forEach(document.querySelectorAll('li'), (el, i) => {
el.textContent = `Item ${i}`;
});Methods:
| Method | Signature | Description |
|---------------------|--------------------------------|----------------------------------------|
| hasClass | (elem, className) => boolean | Check if element has class (null-safe) |
| addClass | (elems, className) => this | Add class to element(s) |
| removeClass | (elems, className) => this | Remove class from element(s) |
| removeAllChildren | (node) => void | Remove all child nodes |
| fromHtml | (html) => DocumentFragment | Parse HTML string to fragment |
| forEach | (elements, callback) => void | Iterate over ArrayLike<Element> |
Cookies
Full-featured cookie management with unicode support, SameSite, and domain/path control. Based on Mozilla's cookie framework.
import {docCookies} from '@changke/staticnext-lib';
// Set a cookie (expires in 3600 seconds, path /, SameSite=Lax)
docCookies.setItem('token', 'abc123', 3600, '/', undefined, true, 'lax');
// Get
docCookies.getItem('token'); // => 'abc123'
// Check existence
docCookies.hasItem('token'); // => true
// List all cookie names
docCookies.keys(); // => ['token', ...]
// Remove
docCookies.removeItem('token', '/');
// Clear all
docCookies.clear('/');Methods:
| Method | Signature | Description |
|--------------|------------------------------------------------------------------------|--------------------------|
| getItem | (key) => string \| null | Get decoded cookie value |
| setItem | (key, value, expiry?, path?, domain?, secure?, sameSite?) => boolean | Set a cookie |
| removeItem | (key, path?, domain?, secure?, sameSite?) => boolean | Remove a cookie |
| hasItem | (key) => boolean | Check if cookie exists |
| keys | () => string[] | List all cookie names |
| clear | (path?, domain?, secure?, sameSite?) => void | Remove all cookies |
The expiry parameter accepts: a number (max-age in seconds, Infinity for persistent),
a string (expires date string), or a Date object.
The sameSite parameter accepts: 'lax', 'strict', 'none', 'no_restriction',
true (= lax), 1 (= lax), or a negative number (= none).
Loader
Lazy loader for CSS and JS files with deduplication.
import {loader} from '@changke/staticnext-lib';
// Load CSS (returns the <link> element, or null if already loaded)
const linkEl = loader.loadCSS('/styles/theme.css');
// Load JS with callbacks
loader.loadJS('/scripts/chart.js', () => {
console.log('Chart library loaded');
}, (err) => {
console.error('Failed to load:', err.message);
});
// Load JS with crossorigin attribute
loader.loadJS('https://cdn.example.com/lib.js', onLoad, onError, true);
// Reset internal tracking (useful for testing)
loader.reset();Methods:
| Method | Signature | Description |
|-----------|--------------------------------------------|-----------------------------------|
| loadJS | (src, cb?, errCb?, crossorigin?) => void | Load a JS file via <script> tag |
| loadCSS | (href) => HTMLLinkElement \| null | Load a CSS file via <link> tag |
| reset | () => void | Clear all internal tracking state |
Files are deduplicated by URL -- requesting the same file twice will call the callback immediately (JS) or return null (CSS).
Logger
Console logging wrapper with mute/unmute and formatted output.
import {logger} from '@changke/staticnext-lib';
// Formatted log (with colors and timestamp)
logger.log('MyComponent', 'initialized successfully');
// Raw object log
logger.log('MyComponent', {users: 42, active: true}, true);
// Log as error
logger.log('MyComponent', 'something failed', false, console.error);
// Suppress all logging
logger.mute();
logger.log('MyComponent', 'this is silent');
// Re-enable
logger.unmute();Methods:
| Method | Signature | Description |
|----------|--------------------------------------|---------------------------------------|
| log | (name, msg, raw?, conLog?) => void | Log a message with module name prefix |
| mute | () => void | Suppress all logging output |
| unmute | () => void | Re-enable logging output |
When raw is false (default), output is color-formatted with CSS in the console.
When raw is true, a plain object {time, name, msg} is logged.
Module
Base class for UI modules. Provides PubSub and Logger integration out of the box. Subclass this to create components that can be auto-initialized on DOM elements.
import {Module} from '@changke/staticnext-lib';
class MyWidget extends Module {
constructor(root: Element) {
super(root);
this.name_ = 'myWidget';
}
start(): void {
this.log('started');
this.subscribe('app:ready', data => {
this.log('app is ready');
});
}
static override attachTo(root: Element): MyWidget {
return new MyWidget(root);
}
}Properties:
| Property | Type | Description |
|----------|-----------|-----------------------------------------------------------|
| root_ | Element | The DOM element this module is attached to |
| name_ | string | Module name (default: 'module'), override in subclasses |
Methods:
| Method | Signature | Description |
|-----------------|----------------------------------|--------------------------------------------------------------------|
| attachTo | static (root) => Module | Factory method to create and return a new instance |
| start | () => void | Lifecycle hook, called after initialization (override in subclass) |
| subscribe | (topic, handler) => Subscribed | Subscribe to a PubSub topic |
| subscribeOnce | (topic, handler) => void | Subscribe to a topic once |
| publish | (topic, data?) => void | Publish to a PubSub topic |
| log | (msg, raw?, error?) => void | Log a message prefixed with the module name |
autoInit
Auto-initializes modules on DOM elements marked with data-mod-name attributes.
<!-- HTML markup -->
<div data-mod-name="myWidget">...</div>
<div data-mod-name="carousel" data-mod-nocss>...</div>
<div data-mod-name="skipped" data-mod-bypass>not initialized</div>import {autoInit} from '@changke/staticnext-lib';
import type {Attachable} from '@changke/staticnext-lib/dist/services/auto-init.js';
// Register a module constructor
autoInit.register('myWidget', MyWidget as unknown as Attachable);
// Initialize all modules in the document
autoInit(document);
// Or within a specific container
autoInit(document.getElementById('app')!);
// Deregister
autoInit.deregister('myWidget');
autoInit.deregisterAll();Data attributes:
| Attribute | Description |
|-------------------|----------------------------------------|
| data-mod-name | Module name to initialize (required) |
| data-mod-bypass | Skip initialization for this element |
| data-mod-nojs | Skip JS loading (CSS only) |
| data-mod-nocss | Skip CSS loading (JS only) |
| data-inited | Set automatically after initialization |
Function signature:
function autoInit(root?: Document | Element, warn?, loader?, assetPath?): void;When loader and assetPath are provided, CSS and JS files are loaded from a
conventional path: ${assetPath.modules}/mod-${name}/${name}.{css,js}.
Static methods:
| Method | Signature | Description |
|-----------------|-------------------------------|------------------------------------|
| register | (name, Ctor, warn?) => void | Register a module constructor |
| deregister | (name) => void | Remove a registered constructor |
| deregisterAll | () => void | Remove all registered constructors |
getUrlPath
Extracts the base URL path from a <script> element in the document.
import {getUrlPath} from '@changke/staticnext-lib';
// Given: <script src="https://cdn.example.com/assets/js/entry.js"></script>
getUrlPath('entry.js');
// => 'https://cdn.example.com/assets/js'
// Returns '/assets/js' if the script element is not foundDevelopment
# Install dependencies
npm install
# Lint
npm run lint
# Run all tests (headless Chromium via Puppeteer)
npm test
# Run a single test file
npx wtr ./test/services/pubsub.test.ts --node-resolve --puppeteer
# Build (TypeScript to ESM JavaScript + declarations)
npm run build
# Full pre-publish check (lint + test + build)
npm run prepublishOnlyLicense
ISC
