azoxjs 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +214 -0
- package/bin/azox.js +18 -0
- package/core/build.js +218 -0
- package/core/buildError.js +8 -0
- package/core/cli/parseArgs.js +39 -0
- package/core/cli/router.js +76 -0
- package/core/commands/compile.js +35 -0
- package/core/commands/create.js +146 -0
- package/core/commands/dev.js +127 -0
- package/core/commands/doctor.js +68 -0
- package/core/commands/help.js +31 -0
- package/core/commands/version.js +7 -0
- package/core/compiler/compileToJs.js +139 -0
- package/core/compiler/index.js +4 -0
- package/core/compiler/parser.js +317 -0
- package/core/compiler/resolveComponents.js +184 -0
- package/core/dev/liveReload.js +20 -0
- package/core/dev/server.js +179 -0
- package/core/dev/watcher.js +52 -0
- package/core/index.js +7 -0
- package/core/meta.js +15 -0
- package/core/reactivity/signal.js +48 -0
- package/core/renderer/renderToHtml.js +67 -0
- package/core/routes.js +78 -0
- package/package.json +47 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// `azox create <name>` — scaffolds a new Azox project.
|
|
2
|
+
//
|
|
3
|
+
// Generated projects import the runtime by its package name
|
|
4
|
+
// ("azox/reactivity"), which is what will work once Azox is on npm.
|
|
5
|
+
// Until then `npm link azox` from the framework checkout makes the
|
|
6
|
+
// same specifier resolve locally, so the scaffold never has to bake
|
|
7
|
+
// in a brittle relative path.
|
|
8
|
+
|
|
9
|
+
import { mkdirSync, writeFileSync, existsSync } from 'node:fs';
|
|
10
|
+
import { resolve, join } from 'node:path';
|
|
11
|
+
|
|
12
|
+
import { BANNER, VERSION } from '../meta.js';
|
|
13
|
+
|
|
14
|
+
export function createCommand({ positionals, flags }) {
|
|
15
|
+
const name = positionals[0] ?? flags.name;
|
|
16
|
+
|
|
17
|
+
if (!name) {
|
|
18
|
+
console.error('Azox: missing project name.');
|
|
19
|
+
console.error('Usage: azox create <name>');
|
|
20
|
+
process.exitCode = 1;
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!/^[a-z0-9][a-z0-9._-]*$/i.test(name)) {
|
|
25
|
+
console.error(`Azox: "${name}" is not a valid project name.`);
|
|
26
|
+
console.error('Use letters, digits, dots, dashes or underscores.');
|
|
27
|
+
process.exitCode = 1;
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const targetDir = resolve(process.cwd(), name);
|
|
32
|
+
|
|
33
|
+
if (existsSync(targetDir)) {
|
|
34
|
+
console.error(`Azox: directory "${name}" already exists.`);
|
|
35
|
+
process.exitCode = 1;
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
mkdirSync(join(targetDir, 'pages'), { recursive: true });
|
|
40
|
+
mkdirSync(join(targetDir, 'components'), { recursive: true });
|
|
41
|
+
|
|
42
|
+
const files = {
|
|
43
|
+
'package.json': projectPackageJson(name),
|
|
44
|
+
'pages/index.azox': starterPage(name),
|
|
45
|
+
'pages/about.azox': aboutPage(name),
|
|
46
|
+
'components/Counter.azox': starterComponent(),
|
|
47
|
+
'.gitignore': '.azox/\nnode_modules/\n.DS_Store\n',
|
|
48
|
+
'README.md': projectReadme(name),
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
for (const [path, contents] of Object.entries(files)) {
|
|
52
|
+
writeFileSync(join(targetDir, path), contents, 'utf8');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
console.log(BANNER);
|
|
56
|
+
console.log('');
|
|
57
|
+
console.log(`Created ${name}/`);
|
|
58
|
+
for (const path of Object.keys(files)) console.log(` ${path}`);
|
|
59
|
+
console.log('');
|
|
60
|
+
console.log('Next:');
|
|
61
|
+
console.log(` cd ${name}`);
|
|
62
|
+
console.log(' npm install');
|
|
63
|
+
console.log(' npx azox dev');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function projectPackageJson(name) {
|
|
67
|
+
return `${JSON.stringify(
|
|
68
|
+
{
|
|
69
|
+
name,
|
|
70
|
+
version: '0.1.0',
|
|
71
|
+
private: true,
|
|
72
|
+
type: 'module',
|
|
73
|
+
scripts: {
|
|
74
|
+
dev: 'azox dev',
|
|
75
|
+
build: 'azox compile',
|
|
76
|
+
},
|
|
77
|
+
devDependencies: {
|
|
78
|
+
azoxjs: `^${VERSION}`,
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
null,
|
|
82
|
+
2
|
|
83
|
+
)}\n`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function starterPage(name) {
|
|
87
|
+
return `<script>
|
|
88
|
+
import Counter from '../components/Counter.azox';
|
|
89
|
+
import { signal } from 'azox/reactivity';
|
|
90
|
+
|
|
91
|
+
const count = signal(0);
|
|
92
|
+
</script>
|
|
93
|
+
|
|
94
|
+
<main class="page">
|
|
95
|
+
<h1>${name}</h1>
|
|
96
|
+
<p>Built with Azox.</p>
|
|
97
|
+
|
|
98
|
+
<Counter label="Clicks" value={count()} />
|
|
99
|
+
|
|
100
|
+
<button on:click={() => count.set(count() + 1)}>
|
|
101
|
+
Add one
|
|
102
|
+
</button>
|
|
103
|
+
|
|
104
|
+
<p><a href="/about">About</a></p>
|
|
105
|
+
</main>
|
|
106
|
+
`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// pages/about.azox is served at /about — the file layout is the
|
|
110
|
+
// routing table.
|
|
111
|
+
function aboutPage(name) {
|
|
112
|
+
return `<main class="page">
|
|
113
|
+
<h1>About</h1>
|
|
114
|
+
<p>${name} is built with Azox.</p>
|
|
115
|
+
<p><a href="/">Home</a></p>
|
|
116
|
+
</main>
|
|
117
|
+
`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Components take props and render markup. State lives in the page
|
|
121
|
+
// that uses them.
|
|
122
|
+
function starterComponent() {
|
|
123
|
+
return `<script>
|
|
124
|
+
const { label, value } = props();
|
|
125
|
+
</script>
|
|
126
|
+
|
|
127
|
+
<p class="counter">{label}: {value}</p>
|
|
128
|
+
`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function projectReadme(name) {
|
|
132
|
+
return `# ${name}
|
|
133
|
+
|
|
134
|
+
An Azox project.
|
|
135
|
+
|
|
136
|
+
## Build
|
|
137
|
+
|
|
138
|
+
\`\`\`bash
|
|
139
|
+
azox compile
|
|
140
|
+
\`\`\`
|
|
141
|
+
|
|
142
|
+
Output lands in \`.azox/build/\`.
|
|
143
|
+
`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const toPosix = (p) => p.split('\\').join('/');
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// `azox dev` — builds the project, serves it, and rebuilds and
|
|
2
|
+
// reloads the browser whenever a source file changes.
|
|
3
|
+
//
|
|
4
|
+
// azox dev
|
|
5
|
+
// azox dev --port=5000 --host=0.0.0.0 --open
|
|
6
|
+
|
|
7
|
+
import { resolve } from 'node:path';
|
|
8
|
+
import { existsSync } from 'node:fs';
|
|
9
|
+
|
|
10
|
+
import { buildAll, BuildError, PAGES_DIR, BUILD_DIR } from '../build.js';
|
|
11
|
+
import { createDevServer } from '../dev/server.js';
|
|
12
|
+
import { watchDirectory } from '../dev/watcher.js';
|
|
13
|
+
import { injectLiveReload } from '../dev/liveReload.js';
|
|
14
|
+
import { BANNER } from '../meta.js';
|
|
15
|
+
|
|
16
|
+
const DEFAULT_PORT = 4321;
|
|
17
|
+
const DEFAULT_HOST = 'localhost';
|
|
18
|
+
const MAX_PORT_ATTEMPTS = 10;
|
|
19
|
+
|
|
20
|
+
export async function devCommand({ flags }) {
|
|
21
|
+
const projectDir = process.cwd();
|
|
22
|
+
const pagesDir = resolve(projectDir, PAGES_DIR);
|
|
23
|
+
|
|
24
|
+
if (!existsSync(pagesDir)) {
|
|
25
|
+
console.error(`Azox: no ${PAGES_DIR}/ directory in ${projectDir}.`);
|
|
26
|
+
console.error('Run "azox create <name>" to start a project.');
|
|
27
|
+
process.exitCode = 1;
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const host = typeof flags.host === 'string' ? flags.host : DEFAULT_HOST;
|
|
32
|
+
const requestedPort = Number(flags.port) || DEFAULT_PORT;
|
|
33
|
+
|
|
34
|
+
console.log(BANNER);
|
|
35
|
+
console.log('');
|
|
36
|
+
|
|
37
|
+
// Build once up front so the first request is served from output
|
|
38
|
+
// that already exists.
|
|
39
|
+
const built = rebuild(projectDir);
|
|
40
|
+
if (!built.ok) reportFailure(built.error);
|
|
41
|
+
|
|
42
|
+
const dev = createDevServer({ rootDir: resolve(projectDir, BUILD_DIR) });
|
|
43
|
+
dev.setBuildError(built.ok ? null : built.error);
|
|
44
|
+
|
|
45
|
+
let port;
|
|
46
|
+
try {
|
|
47
|
+
port = await listenOnFreePort(dev, requestedPort, host);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error(`Azox: could not start the dev server — ${error.message}`);
|
|
50
|
+
process.exitCode = 1;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const url = `http://${host}:${port}`;
|
|
55
|
+
console.log(` Local: ${url}`);
|
|
56
|
+
console.log(` Pages: ${built.ok ? built.results.length : 0}`);
|
|
57
|
+
console.log('');
|
|
58
|
+
console.log(' Watching for changes. Press Ctrl+C to stop.');
|
|
59
|
+
console.log('');
|
|
60
|
+
|
|
61
|
+
const stopWatching = watchDirectory(
|
|
62
|
+
pagesDir,
|
|
63
|
+
(filename) => {
|
|
64
|
+
const result = rebuild(projectDir);
|
|
65
|
+
dev.setBuildError(result.ok ? null : result.error);
|
|
66
|
+
|
|
67
|
+
if (result.ok) {
|
|
68
|
+
console.log(` rebuilt${filename ? ` (${filename})` : ''}`);
|
|
69
|
+
} else {
|
|
70
|
+
reportFailure(result.error);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Reload either way: on failure the browser picks up the error
|
|
74
|
+
// page the server renders in place of the build output.
|
|
75
|
+
dev.reload();
|
|
76
|
+
},
|
|
77
|
+
{ filter: (filename) => filename.endsWith('.azox') }
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const shutdown = async () => {
|
|
81
|
+
stopWatching();
|
|
82
|
+
await dev.close();
|
|
83
|
+
console.log('\nAzox: dev server stopped.');
|
|
84
|
+
process.exit(0);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
process.on('SIGINT', shutdown);
|
|
88
|
+
process.on('SIGTERM', shutdown);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function rebuild(projectDir) {
|
|
92
|
+
try {
|
|
93
|
+
return { ok: true, results: buildAll(projectDir, { transformHtml: injectLiveReload }) };
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (!(error instanceof BuildError)) throw error;
|
|
96
|
+
return { ok: false, error };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function reportFailure(error) {
|
|
101
|
+
console.error(` build failed: ${error.message}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// A dev server that dies because the port is taken is a bad first
|
|
105
|
+
// impression, so step forward until a free one is found.
|
|
106
|
+
async function listenOnFreePort(dev, startPort, host) {
|
|
107
|
+
for (let offset = 0; offset < MAX_PORT_ATTEMPTS; offset++) {
|
|
108
|
+
const port = startPort + offset;
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
await new Promise((done, fail) => {
|
|
112
|
+
dev.server.once('error', fail);
|
|
113
|
+
dev.listen(port, host).then(() => {
|
|
114
|
+
dev.server.removeListener('error', fail);
|
|
115
|
+
done();
|
|
116
|
+
}, fail);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
if (offset > 0) console.log(` Port ${startPort} was busy, using ${port}.`);
|
|
120
|
+
return port;
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (error.code !== 'EADDRINUSE') throw error;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
throw new Error(`ports ${startPort}-${startPort + MAX_PORT_ATTEMPTS - 1} are all in use`);
|
|
127
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// `azox doctor` — checks that the toolchain and the current project
|
|
2
|
+
// are in a working state, and reports what it finds.
|
|
3
|
+
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
|
|
6
|
+
import { parseAzox } from '../compiler/parser.js';
|
|
7
|
+
import { resolveComponents } from '../compiler/resolveComponents.js';
|
|
8
|
+
import { listRoutes, PAGES_DIR } from '../build.js';
|
|
9
|
+
import { BANNER, VERSION } from '../meta.js';
|
|
10
|
+
|
|
11
|
+
const MIN_NODE_MAJOR = 18;
|
|
12
|
+
|
|
13
|
+
export function doctorCommand() {
|
|
14
|
+
const projectDir = process.cwd();
|
|
15
|
+
const checks = [];
|
|
16
|
+
|
|
17
|
+
const nodeMajor = Number(process.versions.node.split('.')[0]);
|
|
18
|
+
checks.push({
|
|
19
|
+
ok: nodeMajor >= MIN_NODE_MAJOR,
|
|
20
|
+
label: `Node ${process.versions.node}`,
|
|
21
|
+
detail: nodeMajor >= MIN_NODE_MAJOR ? null : `Azox needs Node ${MIN_NODE_MAJOR} or newer`,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
checks.push({ ok: true, label: `Azox v${VERSION}` });
|
|
25
|
+
|
|
26
|
+
const routes = listRoutes(projectDir);
|
|
27
|
+
|
|
28
|
+
checks.push({
|
|
29
|
+
ok: routes.length > 0,
|
|
30
|
+
label: `${PAGES_DIR}/ (${routes.length} page${routes.length === 1 ? '' : 's'})`,
|
|
31
|
+
detail: routes.length
|
|
32
|
+
? null
|
|
33
|
+
: `No .azox pages found — run "azox create <name>" to start one`,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Fully resolve every page — parse it and its components — so a
|
|
37
|
+
// broken reference surfaces here rather than halfway through a
|
|
38
|
+
// build.
|
|
39
|
+
for (const route of routes) {
|
|
40
|
+
try {
|
|
41
|
+
resolveComponents(parseAzox(readFileSync(route.sourcePath, 'utf8')), route.sourcePath);
|
|
42
|
+
checks.push({ ok: true, label: `${route.url}`, note: `${PAGES_DIR}/${route.name}.azox` });
|
|
43
|
+
} catch (error) {
|
|
44
|
+
checks.push({
|
|
45
|
+
ok: false,
|
|
46
|
+
label: `${route.url}`,
|
|
47
|
+
note: `${PAGES_DIR}/${route.name}.azox`,
|
|
48
|
+
detail: error.message,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.log(BANNER);
|
|
54
|
+
console.log('');
|
|
55
|
+
for (const check of checks) {
|
|
56
|
+
console.log(` ${check.ok ? '✓' : '✗'} ${check.label}${check.note ? ` (${check.note})` : ''}`);
|
|
57
|
+
if (check.detail) console.log(` ${check.detail}`);
|
|
58
|
+
}
|
|
59
|
+
console.log('');
|
|
60
|
+
|
|
61
|
+
const failures = checks.filter((check) => !check.ok).length;
|
|
62
|
+
if (failures) {
|
|
63
|
+
console.log(`${failures} problem${failures === 1 ? '' : 's'} found.`);
|
|
64
|
+
process.exitCode = 1;
|
|
65
|
+
} else {
|
|
66
|
+
console.log('Everything looks good.');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// `azox help` / `azox --help` / bare `azox`
|
|
2
|
+
//
|
|
3
|
+
// Reads its content straight from the command registry so the help
|
|
4
|
+
// output can never fall out of step with what the CLI actually does.
|
|
5
|
+
|
|
6
|
+
import { BANNER } from '../meta.js';
|
|
7
|
+
|
|
8
|
+
export function helpCommand({ registry }) {
|
|
9
|
+
const names = Object.keys(registry);
|
|
10
|
+
const width = Math.max(...names.map((n) => n.length));
|
|
11
|
+
|
|
12
|
+
console.log(BANNER);
|
|
13
|
+
console.log('');
|
|
14
|
+
console.log('Usage: azox <command> [options]');
|
|
15
|
+
console.log('');
|
|
16
|
+
console.log('Commands:');
|
|
17
|
+
for (const name of names) {
|
|
18
|
+
console.log(` ${name.padEnd(width)} ${registry[name].describe}`);
|
|
19
|
+
}
|
|
20
|
+
console.log('');
|
|
21
|
+
console.log('Options:');
|
|
22
|
+
console.log(' -v, --version Print the Azox version');
|
|
23
|
+
console.log(' -h, --help Show this help');
|
|
24
|
+
console.log('');
|
|
25
|
+
console.log('Examples:');
|
|
26
|
+
for (const name of names) {
|
|
27
|
+
for (const example of registry[name].examples ?? []) {
|
|
28
|
+
console.log(` ${example}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// Turns a parsed .azox AST into a JS module. The generated `render`
|
|
2
|
+
// function builds real DOM nodes and wires each dynamic binding to
|
|
3
|
+
// its own effect — no Virtual DOM tree, no diffing. A signal update
|
|
4
|
+
// touches exactly the text node or attribute it owns.
|
|
5
|
+
|
|
6
|
+
import { relative, dirname, resolve } from 'node:path';
|
|
7
|
+
|
|
8
|
+
let uid = 0;
|
|
9
|
+
const nextId = () => `_el${uid++}`;
|
|
10
|
+
|
|
11
|
+
// sourcePath: absolute path of the .azox file being compiled.
|
|
12
|
+
// outPath: absolute path of the client module being written.
|
|
13
|
+
// runtimeSpecifier: how the emitted module should import the Azox
|
|
14
|
+
// runtime — a bare "azox/reactivity" for user projects, or a
|
|
15
|
+
// relative path when compiling inside the framework itself.
|
|
16
|
+
//
|
|
17
|
+
// Relative import specifiers in the user's <script> are resolved
|
|
18
|
+
// against sourcePath and re-expressed relative to outPath, since
|
|
19
|
+
// compiled output lives in .azox/build/, not next to the page.
|
|
20
|
+
// Bare specifiers are left untouched for the resolver to handle.
|
|
21
|
+
export function compileToModule(ast, { sourcePath, outPath, runtimeSpecifier }) {
|
|
22
|
+
uid = 0;
|
|
23
|
+
const statements = [];
|
|
24
|
+
const rootVar = emitNode(ast.markup, statements, 'root');
|
|
25
|
+
const script = rebaseImports(dropComponentImports(ast.script), dirname(sourcePath), outPath);
|
|
26
|
+
|
|
27
|
+
return `
|
|
28
|
+
import { effect } from '${runtimeSpecifier}';
|
|
29
|
+
${script}
|
|
30
|
+
|
|
31
|
+
export function render(mount) {
|
|
32
|
+
${statements.map((line) => ' ' + line).join('\n')}
|
|
33
|
+
mount.appendChild(${rootVar});
|
|
34
|
+
return ${rootVar};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Hydrate: the SSR markup is already on the page, so clear it and
|
|
38
|
+
// mount the reactive version in its place.
|
|
39
|
+
if (typeof document !== 'undefined') {
|
|
40
|
+
const mount = document.querySelector('[data-azox-root]') ?? document.body;
|
|
41
|
+
mount.innerHTML = '';
|
|
42
|
+
render(mount);
|
|
43
|
+
}
|
|
44
|
+
`.trimStart();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function emitNode(node, statements, fallbackVar) {
|
|
48
|
+
if (!node) return 'null';
|
|
49
|
+
|
|
50
|
+
if (node.type === 'text') {
|
|
51
|
+
return emitText(node, statements, fallbackVar);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// A fragment (from <slot />) has no element of its own; it wraps
|
|
55
|
+
// its children in a DocumentFragment so they land in the parent.
|
|
56
|
+
if (node.type === 'fragment') {
|
|
57
|
+
const varName = nextId();
|
|
58
|
+
statements.push(`const ${varName} = document.createDocumentFragment();`);
|
|
59
|
+
appendChildren(varName, node.children, statements, fallbackVar);
|
|
60
|
+
return varName;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const varName = nextId();
|
|
64
|
+
statements.push(`const ${varName} = document.createElement(${JSON.stringify(node.name)});`);
|
|
65
|
+
|
|
66
|
+
for (const [key, attr] of Object.entries(node.attrs)) {
|
|
67
|
+
emitAttr(varName, key, attr, statements);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
appendChildren(varName, node.children, statements, fallbackVar);
|
|
71
|
+
|
|
72
|
+
return varName;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function appendChildren(parentVar, children, statements, fallbackVar) {
|
|
76
|
+
for (const child of children) {
|
|
77
|
+
const childVar = emitNode(child, statements, fallbackVar);
|
|
78
|
+
if (childVar !== 'null') statements.push(`${parentVar}.appendChild(${childVar});`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function emitText(node, statements, fallbackVar) {
|
|
83
|
+
// Purely static text: one text node, no effect needed.
|
|
84
|
+
if (node.parts.every((p) => p.kind === 'static')) {
|
|
85
|
+
const value = node.parts.map((p) => p.value).join('');
|
|
86
|
+
const varName = nextId();
|
|
87
|
+
statements.push(`const ${varName} = document.createTextNode(${JSON.stringify(value)});`);
|
|
88
|
+
return varName;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Dynamic text: one text node, one effect that rewrites its data.
|
|
92
|
+
const varName = nextId();
|
|
93
|
+
statements.push(`const ${varName} = document.createTextNode('');`);
|
|
94
|
+
const expr = node.parts
|
|
95
|
+
.map((p) => (p.kind === 'static' ? JSON.stringify(p.value) : `String(${p.expr})`))
|
|
96
|
+
.join(' + ');
|
|
97
|
+
statements.push(`effect(() => { ${varName}.data = ${expr}; });`);
|
|
98
|
+
return varName;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function emitAttr(varName, key, attr, statements) {
|
|
102
|
+
if (key.startsWith('on:')) {
|
|
103
|
+
const event = key.slice(3);
|
|
104
|
+
statements.push(`${varName}.addEventListener(${JSON.stringify(event)}, ${attr.expr});`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (attr.kind === 'static') {
|
|
109
|
+
statements.push(`${varName}.setAttribute(${JSON.stringify(key)}, ${JSON.stringify(attr.value)});`);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Dynamic attribute: wrap in its own effect, same fine-grained rule as text.
|
|
114
|
+
statements.push(`effect(() => { ${varName}.setAttribute(${JSON.stringify(key)}, String(${attr.expr})); });`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Component imports are resolved at build time and inlined, so the
|
|
118
|
+
// .azox specifier must not survive into JavaScript the browser loads.
|
|
119
|
+
function dropComponentImports(script) {
|
|
120
|
+
return script.replace(/^\s*import\s+[A-Z]\w*\s+from\s+['"][^'"]+\.azox['"]\s*;?\s*$/gm, '');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Rewrites every relative import specifier in the user's <script>
|
|
124
|
+
// block so it still resolves once the module lives in outPath
|
|
125
|
+
// instead of next to sourceDir.
|
|
126
|
+
function rebaseImports(script, sourceDir, outPath) {
|
|
127
|
+
return script.replace(
|
|
128
|
+
/(from\s+|import\s+)(['"])(\.[^'"]*)\2/g,
|
|
129
|
+
(full, keyword, quote, specifier) =>
|
|
130
|
+
`${keyword}${quote}${rebaseSpecifier(resolve(sourceDir, specifier), outPath)}${quote}`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Re-expresses an absolute target path as a path relative to outPath.
|
|
135
|
+
function rebaseSpecifier(absoluteTarget, outPath) {
|
|
136
|
+
let rebased = relative(dirname(outPath), absoluteTarget);
|
|
137
|
+
if (!rebased.startsWith('.')) rebased = `./${rebased}`;
|
|
138
|
+
return rebased;
|
|
139
|
+
}
|