Skip to content
Closed
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
30 changes: 28 additions & 2 deletions demo/demo-sw.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,34 @@ self.addEventListener('install', function(event) {
self.addEventListener('activate', () => self.clients.claim());

// Intercept fetch requests for modules and compile the intercepted typescript.
// Whether a URL is one we're willing to intercept and compile.
// Not just https: a service worker also runs on http://localhost and
// http://127.0.0.1, because those are secure contexts. Testing the scheme alone
// makes the whole demo silently do nothing when served locally over http - no
// TypeScript is ever transpiled, so the page loads but none of its code runs.
const isCompilableUrl = (url) => {
try {
const u = new URL(url);
if (u.protocol === 'https:') { return true; }
return u.protocol === 'http:'
&& (u.hostname === 'localhost' || u.hostname === '127.0.0.1' || u.hostname === '[::1]');
} catch {
return false;
}
};

// Local development should always recompile rather than serve a stale build.
const isLocalUrl = (url) => {
try {
const u = new URL(url);
return u.hostname === 'localhost' || u.hostname === '127.0.0.1' || u.hostname === '[::1]';
} catch {
return false;
}
};

self.addEventListener('fetch', (event) => {
if (!event.request.url.startsWith('https://')) {
if (!isCompilableUrl(event.request.url)) {
return;
}

Expand Down Expand Up @@ -107,7 +133,7 @@ const transpileTypeScript = async (requestUrl) => {
};

const transpiledResponse = new Response(transpiledCode, responseOptions);
if (!requestUrl.startsWith('https://localhost') && requestUrl.startsWith('https://')) {
if (!isLocalUrl(requestUrl)) {
typescriptCache.put(requestUrl, transpiledResponse.clone());
}

Expand Down
16 changes: 11 additions & 5 deletions demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,11 @@

// ChromeOS works out of the box!

// WebUSB requires HTTPS, show an error if the page wasn't loaded securely.
if (location.protocol !== 'https:') {
// WebUSB requires a secure context, show an error if the page wasn't
// loaded in one. Note this is NOT the same as checking for https:
// http://localhost (and 127.0.0.1, and file:) are secure contexts too,
// which is what makes local development possible at all.
if (!window.isSecureContext) {
document.getElementById('urlNotSecure').classList.remove('d-none');
}

Expand Down Expand Up @@ -113,7 +116,10 @@
vendorId: 0x2730, // Citizen
},
{
vendorId: 0x04B8 // Epson
vendorId: 0x04B8, // Epson
},
{
vendorId: 0x0aa7, // Wincor Nixdorf (TH230, ESC/POS)
}
]
}
Expand Down Expand Up @@ -457,8 +463,8 @@ <h4>Your browser doesn't support WebUSB</h4>
<p>This demo uses WebUSB to function and your browser doesn't seem to have that available. Try <a href="https://caniuse.com/webusb">using one that has WebUSB</a>.</p>
</div>
<div class="alert alert-warning d-none" role="alert" id="urlNotSecure">
<h4>WebUSB requires HTTPS</h4>
<p>It looks like this URL is not using HTTPS, and WebUSB <a href="https://developer.mozilla.org/en-US/docs/Web/API/WebUSB_API">only works in a secure context.</a> You'll need to load this page over HTTPS instead.</p>
<h4>WebUSB requires a secure context</h4>
<p>This page was not loaded in a <a href="https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts">secure context</a>, and WebUSB <a href="https://developer.mozilla.org/en-US/docs/Web/API/WebUSB_API">only works in one.</a> Load it over HTTPS, or from <code>localhost</code>, which counts as secure for local development.</p>
</div>
<div class="alert alert-info" role="alert" id="loadingIndicator">
<h4>Loading....</h4>
Expand Down
5 changes: 4 additions & 1 deletion demo/test_index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ const printerMgr: PrinterManager = new WebDevices.UsbDeviceManager(
vendorId: 0x2730, // Citizen
},
{
vendorId: 0x04B8 // Epson
vendorId: 0x04B8, // Epson
},
{
vendorId: 0x0aa7, // Wincor Nixdorf (TH230, ESC/POS)
}
]
}
Expand Down
59 changes: 37 additions & 22 deletions demo/ts-browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,18 @@ window.addEventListener('DOMContentLoaded', async () => {
const scripts = document.getElementsByTagName('script');

// Register the Service Worker which will polyfill the HTML module behavior.
await navigator.serviceWorker.register('./demo-sw.js');
//
// Prefer the widest scope we're allowed. The import map reaches outside this
// directory (../src/...), and a worker registered at its own default scope of
// ./ cannot intercept those requests, so the browser would try to execute the
// raw .ts and refuse it for having a non-JavaScript MIME type. Root scope
// needs the server to send `Service-Worker-Allowed: /`; when it doesn't, that
// registration throws and we fall back to the default scope.
try {
await navigator.serviceWorker.register('./demo-sw.js', { scope: '/' });
} catch {
await navigator.serviceWorker.register('./demo-sw.js');
}

// Unfortunately, we have to wait for the Service Worker to ready before
// actually loading the application so that it can intercept the HTML requests.
Expand All @@ -26,29 +37,33 @@ window.addEventListener('DOMContentLoaded', async () => {
// Next up is to compile the inline typescript present on the page.
// Pick up their contents, yeet them at the compiler, and then load the result as a URL blob
// so that modules will load properly.
let pending = [];
for (let i = 0; i < scripts.length; i++) {
if (scripts[i].type === 'text/typescript') {
pending.push(
new Promise(resolve => {
worker.active.postMessage([`Inline script tag ${i}`, scripts[i].innerHTML]);

navigator.serviceWorker.onmessage = async ({ data: transpiled }) => {
// In order for the browser to treat this as the es6 module it is
// we must trick it into 'loading' it. We encode it into a blob URL
// and then 'import' that.

// TODO: Post it externally and then load it inline so it looks more normal?
var scriptAsBlob = createBlob(transpiled, 'text/javascript')
await import(scriptAsBlob);
resolve();
}
}),
)
}
//
// These are done strictly one at a time. The worker's reply carries no id
// saying which script it belongs to, and it compiles asynchronously, so
// replies can come back in a different order than the requests went out.
// Posting everything up front and pairing replies positionally would then
// execute the wrong blob. Waiting for each reply in turn is slower but is
// the only ordering we can actually rely on.
const tsScripts = Array.from(scripts).filter(s => s.type === 'text/typescript');
for (let i = 0; i < tsScripts.length; i++) {
const transpiled = await new Promise(resolve => {
const onMessage = ({ data }) => {
navigator.serviceWorker.removeEventListener('message', onMessage);
resolve(data);
};
navigator.serviceWorker.addEventListener('message', onMessage);
worker.active.postMessage([`Inline script tag ${i}`, tsScripts[i].innerHTML]);
});

// In order for the browser to treat this as the es6 module it is
// we must trick it into 'loading' it. We encode it into a blob URL
// and then 'import' that.

// TODO: Post it externally and then load it inline so it looks more normal?
const scriptAsBlob = createBlob(transpiled, 'text/javascript');
await import(scriptAsBlob);
}

await Promise.all(pending);
window.dispatchEvent(tsTranspiledEvent);
});
});
133 changes: 115 additions & 18 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,125 @@
// @ts-check

import { defineConfig, globalIgnores } from 'eslint/config';
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import globals from "globals";

export default tseslint.config(
export default defineConfig(
globalIgnores([
'dist/',
'coverage/',
'docs/',
'demo/',
// Vendored third-party JS, not ours to lint.
'src/ReceiptLine/*.js',
'**/node_modules/**',
]),
eslint.configs.recommended,
...tseslint.configs.recommended,
tseslint.configs.strictTypeChecked,
tseslint.configs.stylisticTypeChecked,
{
ignores: [
'dist/',
'**/node_modules/**',
'src/ReceiptLine/ESCPOS.js',
'src/ReceiptLine/RECEIPTLINE.js'
]
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
parserOptions: {
// Type-aware linting. projectService picks up tsconfig.json automatically;
// allowDefaultProject covers the root config files that tsconfig excludes.
projectService: {
allowDefaultProject: ['eslint.config.js', 'vite.config.ts'],
},
},
},
linterOptions: {
reportUnusedDisableDirectives: 'error',
},
rules: {
// --- Real-bug rules, explicitly errors ---
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/no-misused-promises': 'error',
'@typescript-eslint/require-await': 'error',
'@typescript-eslint/await-thenable': 'error',
'@typescript-eslint/no-unnecessary-condition': 'error',
'@typescript-eslint/no-unsafe-argument': 'error',
'@typescript-eslint/no-unsafe-assignment': 'error',
'@typescript-eslint/no-unsafe-call': 'error',
'@typescript-eslint/no-unsafe-member-access': 'error',
'@typescript-eslint/no-unsafe-return': 'error',
'@typescript-eslint/no-unsafe-declaration-merging': 'error',
'@typescript-eslint/no-unsafe-enum-comparison': 'error',
'@typescript-eslint/no-unsafe-function-type': 'error',
'@typescript-eslint/no-unsafe-unary-minus': 'error',
'consistent-return': 'error',
'require-await': 'off', // superseded by the type-aware version above

// --- Aesthetic noise, off ---
'@typescript-eslint/naming-convention': 'off',
'@typescript-eslint/member-ordering': 'off',
'@typescript-eslint/explicit-member-accessibility': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/consistent-type-definitions': 'off',
'@typescript-eslint/prefer-nullish-coalescing': 'off',
'@typescript-eslint/class-literal-property-style': 'off',
'@typescript-eslint/array-type': 'off',
'@typescript-eslint/consistent-indexed-object-style': 'off',
'@typescript-eslint/consistent-generic-constructors': 'off',

// Tests reach into private members deliberately; the bracket form is the
// supported escape hatch and autofixing it away breaks compilation.
'@typescript-eslint/dot-notation': ['error', { allowPrivateClassPropertyAccess: true }],
// Numbers in log and error strings are fine.
'@typescript-eslint/restrict-template-expressions': ['error', { allowNumber: true }],
// A `default` clause is an intentional statement that the remaining cases
// are handled elsewhere; several parsers here deliberately cover only the
// subcommands they own.
'@typescript-eslint/switch-exhaustiveness-check': ['error', { considerDefaultExhaustiveForUnions: true }],

// --- Codebase idioms, deliberately off ---
// Enum members are computed from shared constants in the ReceiptLine parser.
'@typescript-eslint/prefer-literal-enum-member': 'off',
// Static-only classes are used as namespaces for command sets.
'@typescript-eslint/no-extraneous-class': 'off',
// Parameter properties read as useless constructors to this rule.
'@typescript-eslint/no-useless-constructor': 'off',
'@typescript-eslint/no-empty-function': 'off',
},
},
{
files: [
'demo/**/*.{js,ts}'
],
languageOptions: {
globals: {
...globals.browser
}
}
}
// Test files: relax the rules that only make sense for library code.
files: ['**/*.test.ts'],
rules: {
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-argument': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-misused-spread': 'off',
'@typescript-eslint/require-await': 'off',
'@typescript-eslint/no-confusing-void-expression': 'off',
},
},
{
// src/ReceiptLine/Parser.ts is a TypeScript port of the ReceiptLine
// reference implementation, kept deliberately close to the original so it
// can be diffed against upstream. It is still type-checked; only the
// style-of-code rules that would force it to diverge are relaxed.
files: ['src/ReceiptLine/Parser.ts'],
rules: {
'@typescript-eslint/no-unsafe-enum-comparison': 'off',
'@typescript-eslint/no-unsafe-argument': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/switch-exhaustiveness-check': 'off',
'@typescript-eslint/no-unnecessary-condition': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'no-useless-assignment': 'off',
},
},
{
// vite.config.ts pulls in plugins that ship no types.
files: ['vite.config.ts'],
rules: {
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
},
},
);
Loading