tessera-learn 0.0.1
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/AGENTS.md +1228 -0
- package/LICENSE +21 -0
- package/README.md +21 -0
- package/dist/plugin/index.d.ts +7 -0
- package/dist/plugin/index.d.ts.map +1 -0
- package/dist/plugin/index.js +1239 -0
- package/dist/plugin/index.js.map +1 -0
- package/package.json +77 -0
- package/src/archiver.d.ts +27 -0
- package/src/components/Accordion.svelte +32 -0
- package/src/components/AccordionItem.svelte +144 -0
- package/src/components/Audio.svelte +38 -0
- package/src/components/Callout.svelte +81 -0
- package/src/components/Carousel.svelte +194 -0
- package/src/components/CarouselSlide.svelte +32 -0
- package/src/components/DefaultLayout.svelte +108 -0
- package/src/components/FillInTheBlank.svelte +345 -0
- package/src/components/Image.svelte +47 -0
- package/src/components/Matching.svelte +513 -0
- package/src/components/MultipleChoice.svelte +363 -0
- package/src/components/Quiz.svelte +569 -0
- package/src/components/RevealModal.svelte +228 -0
- package/src/components/Sorting.svelte +663 -0
- package/src/components/Video.svelte +118 -0
- package/src/components/index.ts +15 -0
- package/src/components/quiz-payload.ts +71 -0
- package/src/components/util.ts +24 -0
- package/src/index.ts +56 -0
- package/src/plugin/export.ts +264 -0
- package/src/plugin/index.ts +464 -0
- package/src/plugin/layout.ts +55 -0
- package/src/plugin/manifest.ts +330 -0
- package/src/plugin/quiz.ts +65 -0
- package/src/plugin/validation.ts +838 -0
- package/src/runtime/App.svelte +435 -0
- package/src/runtime/ErrorPage.svelte +14 -0
- package/src/runtime/LoadingSkeleton.svelte +26 -0
- package/src/runtime/Sidebar.svelte +76 -0
- package/src/runtime/access.ts +55 -0
- package/src/runtime/adapters/cmi5.ts +341 -0
- package/src/runtime/adapters/discovery.ts +38 -0
- package/src/runtime/adapters/index.ts +99 -0
- package/src/runtime/adapters/retry.ts +284 -0
- package/src/runtime/adapters/scorm12.ts +172 -0
- package/src/runtime/adapters/scorm2004.ts +162 -0
- package/src/runtime/adapters/web.ts +62 -0
- package/src/runtime/contexts.ts +76 -0
- package/src/runtime/duration.ts +29 -0
- package/src/runtime/hooks.svelte.ts +543 -0
- package/src/runtime/interaction-format.ts +132 -0
- package/src/runtime/interaction.ts +96 -0
- package/src/runtime/navigation.svelte.ts +117 -0
- package/src/runtime/persistence.ts +56 -0
- package/src/runtime/progress.svelte.ts +168 -0
- package/src/runtime/quiz-policy.ts +227 -0
- package/src/runtime/slugify.ts +17 -0
- package/src/runtime/types.ts +92 -0
- package/src/runtime/xapi/agent-rules.ts +93 -0
- package/src/runtime/xapi/client.ts +133 -0
- package/src/runtime/xapi/derive-actor.ts +90 -0
- package/src/runtime/xapi/publisher.ts +604 -0
- package/src/runtime/xapi/registry.ts +38 -0
- package/src/runtime/xapi/setup.ts +250 -0
- package/src/runtime/xapi/types.ts +106 -0
- package/src/runtime/xapi/uuid.ts +21 -0
- package/src/runtime/xapi/validation.ts +71 -0
- package/src/runtime/xapi/version.ts +23 -0
- package/src/virtual.d.ts +16 -0
- package/styles/base.css +194 -0
- package/styles/layout.css +408 -0
- package/styles/theme.css +36 -0
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
import type { Plugin, ResolvedConfig, ViteDevServer } from 'vite';
|
|
2
|
+
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { dirname, resolve } from 'node:path';
|
|
5
|
+
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync, unlinkSync, cpSync, mkdirSync } from 'node:fs';
|
|
6
|
+
import { generateManifest, extractDefaultExportObjectLiteral } from './manifest.js';
|
|
7
|
+
import JSON5 from 'json5';
|
|
8
|
+
import type { Manifest } from './manifest.js';
|
|
9
|
+
import { validateProject } from './validation.js';
|
|
10
|
+
import { runExport } from './export.js';
|
|
11
|
+
import { tesseraLayoutPlugin } from './layout.js';
|
|
12
|
+
import { tesseraQuizPlugin } from './quiz.js';
|
|
13
|
+
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = dirname(__filename);
|
|
16
|
+
|
|
17
|
+
// Resolve the runtime directory where App.svelte lives
|
|
18
|
+
function resolveRuntimeDir(): string {
|
|
19
|
+
const packageRoot = resolve(__dirname, '..', '..');
|
|
20
|
+
return resolve(packageRoot, 'src', 'runtime');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Resolve the framework styles directory
|
|
24
|
+
function resolveStylesDir(): string {
|
|
25
|
+
const packageRoot = resolve(__dirname, '..', '..');
|
|
26
|
+
return resolve(packageRoot, 'styles');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function tesseraPlugin() {
|
|
30
|
+
return [
|
|
31
|
+
svelte({
|
|
32
|
+
compilerOptions: { css: 'injected' },
|
|
33
|
+
}),
|
|
34
|
+
tesseraValidationPlugin(),
|
|
35
|
+
tesseraEntryPlugin(),
|
|
36
|
+
tesseraConfigPlugin(),
|
|
37
|
+
tesseraPagesPlugin(),
|
|
38
|
+
tesseraManifestPlugin(),
|
|
39
|
+
tesseraLayoutPlugin(),
|
|
40
|
+
tesseraQuizPlugin(),
|
|
41
|
+
tesseraExportPlugin(),
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ---------- Entry Plugin ----------
|
|
46
|
+
|
|
47
|
+
const VIRTUAL_ENTRY_ID = 'virtual:tessera-entry';
|
|
48
|
+
const RESOLVED_ENTRY_ID = '\0' + VIRTUAL_ENTRY_ID;
|
|
49
|
+
const VIRTUAL_MAIN_ID = '/virtual:tessera-main';
|
|
50
|
+
const RESOLVED_MAIN_ID = '\0virtual:tessera-main';
|
|
51
|
+
|
|
52
|
+
function tesseraEntryPlugin(): Plugin {
|
|
53
|
+
const runtimeDir = resolveRuntimeDir();
|
|
54
|
+
const stylesDir = resolveStylesDir();
|
|
55
|
+
const appSveltePath = resolve(runtimeDir, 'App.svelte');
|
|
56
|
+
let projectRoot: string;
|
|
57
|
+
let isBuild = false;
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
name: 'tessera:entry',
|
|
61
|
+
enforce: 'pre',
|
|
62
|
+
|
|
63
|
+
configResolved(config: ResolvedConfig) {
|
|
64
|
+
projectRoot = config.root;
|
|
65
|
+
isBuild = config.command === 'build';
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
// For build mode: write index.html so Rollup can find it
|
|
69
|
+
buildStart() {
|
|
70
|
+
if (isBuild) {
|
|
71
|
+
writeFileSync(resolve(projectRoot, 'index.html'), generateIndexHtml(), 'utf-8');
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
// For build mode: clean up temporary index.html and copy assets
|
|
76
|
+
closeBundle() {
|
|
77
|
+
if (isBuild) {
|
|
78
|
+
const htmlPath = resolve(projectRoot, 'index.html');
|
|
79
|
+
if (existsSync(htmlPath)) {
|
|
80
|
+
try { unlinkSync(htmlPath); } catch {}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Copy assets/ directory to dist/assets/ so $assets/ references resolve
|
|
84
|
+
const assetsDir = resolve(projectRoot, 'assets');
|
|
85
|
+
const distAssetsDir = resolve(projectRoot, 'dist', 'assets');
|
|
86
|
+
if (existsSync(assetsDir)) {
|
|
87
|
+
mkdirSync(distAssetsDir, { recursive: true });
|
|
88
|
+
cpSync(assetsDir, distAssetsDir, { recursive: true });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
// Serve index.html for the dev server
|
|
94
|
+
configureServer(server: ViteDevServer) {
|
|
95
|
+
return () => {
|
|
96
|
+
server.middlewares.use(async (req, res, next) => {
|
|
97
|
+
if (req.url === '/' || req.url === '/index.html') {
|
|
98
|
+
const html = generateIndexHtml();
|
|
99
|
+
const transformed = await server.transformIndexHtml(req.url, html);
|
|
100
|
+
res.setHeader('Content-Type', 'text/html');
|
|
101
|
+
res.statusCode = 200;
|
|
102
|
+
res.end(transformed);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
next();
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
resolveId(id) {
|
|
111
|
+
if (id === VIRTUAL_ENTRY_ID) return RESOLVED_ENTRY_ID;
|
|
112
|
+
if (id === VIRTUAL_MAIN_ID || id === 'virtual:tessera-main') return RESOLVED_MAIN_ID;
|
|
113
|
+
return null;
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
load(id) {
|
|
117
|
+
if (id === RESOLVED_ENTRY_ID || id === RESOLVED_MAIN_ID) {
|
|
118
|
+
return generateEntryScript(appSveltePath, stylesDir, projectRoot);
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function generateIndexHtml(): string {
|
|
126
|
+
return `<!DOCTYPE html>
|
|
127
|
+
<html lang="en">
|
|
128
|
+
<head>
|
|
129
|
+
<meta charset="UTF-8" />
|
|
130
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
131
|
+
<title>Tessera Course</title>
|
|
132
|
+
</head>
|
|
133
|
+
<body>
|
|
134
|
+
<div id="tessera-root"></div>
|
|
135
|
+
<script type="module" src="/virtual:tessera-main"></script>
|
|
136
|
+
</body>
|
|
137
|
+
</html>`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function generateEntryScript(appSveltePath: string, frameworkStylesDir: string, projectRoot: string): string {
|
|
141
|
+
const normalizedPath = appSveltePath.replace(/\\/g, '/');
|
|
142
|
+
|
|
143
|
+
// Framework CSS imports (theme → base → layout)
|
|
144
|
+
const frameworkCssOrder = ['theme.css', 'base.css', 'layout.css'];
|
|
145
|
+
const frameworkImports = frameworkCssOrder
|
|
146
|
+
.map(file => resolve(frameworkStylesDir, file).replace(/\\/g, '/'))
|
|
147
|
+
.filter(path => existsSync(path))
|
|
148
|
+
.map(path => `import '${path}';`)
|
|
149
|
+
.join('\n');
|
|
150
|
+
|
|
151
|
+
// User CSS imports from project's styles/ directory
|
|
152
|
+
const userStylesDir = resolve(projectRoot, 'styles');
|
|
153
|
+
let userImports = '';
|
|
154
|
+
if (existsSync(userStylesDir)) {
|
|
155
|
+
const userCssFiles = readdirSync(userStylesDir)
|
|
156
|
+
.filter(f => f.endsWith('.css'))
|
|
157
|
+
.sort();
|
|
158
|
+
userImports = userCssFiles
|
|
159
|
+
.map(f => resolve(userStylesDir, f).replace(/\\/g, '/'))
|
|
160
|
+
.map(path => `import '${path}';`)
|
|
161
|
+
.join('\n');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return `// Framework styles
|
|
165
|
+
${frameworkImports}
|
|
166
|
+
// User styles
|
|
167
|
+
${userImports}
|
|
168
|
+
|
|
169
|
+
import { mount } from 'svelte';
|
|
170
|
+
import App from '${normalizedPath}';
|
|
171
|
+
|
|
172
|
+
mount(App, {
|
|
173
|
+
target: document.getElementById('tessera-root'),
|
|
174
|
+
});
|
|
175
|
+
`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ---------- Config Plugin ----------
|
|
179
|
+
|
|
180
|
+
const VIRTUAL_CONFIG_ID = 'virtual:tessera-config';
|
|
181
|
+
const RESOLVED_CONFIG_ID = '\0' + VIRTUAL_CONFIG_ID;
|
|
182
|
+
|
|
183
|
+
function tesseraConfigPlugin(): Plugin {
|
|
184
|
+
let projectRoot: string;
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
name: 'tessera:config',
|
|
188
|
+
enforce: 'pre',
|
|
189
|
+
|
|
190
|
+
config(config) {
|
|
191
|
+
const root = config.root || process.cwd();
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
base: './',
|
|
195
|
+
resolve: {
|
|
196
|
+
alias: {
|
|
197
|
+
'$assets': resolve(root, 'assets'),
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
configResolved(config: ResolvedConfig) {
|
|
204
|
+
projectRoot = config.root;
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
resolveId(id) {
|
|
208
|
+
if (id === VIRTUAL_CONFIG_ID) return RESOLVED_CONFIG_ID;
|
|
209
|
+
return null;
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
load(id) {
|
|
213
|
+
if (id === RESOLVED_CONFIG_ID) {
|
|
214
|
+
const configPath = resolve(projectRoot, 'course.config.js');
|
|
215
|
+
let userConfig: Record<string, any> = {};
|
|
216
|
+
|
|
217
|
+
if (existsSync(configPath)) {
|
|
218
|
+
this.addWatchFile(configPath);
|
|
219
|
+
const objectStr = extractDefaultExportObjectLiteral(readFileSync(configPath, 'utf-8'));
|
|
220
|
+
if (objectStr) {
|
|
221
|
+
try { userConfig = JSON5.parse(objectStr); } catch {}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const merged = {
|
|
226
|
+
title: userConfig.title || 'Untitled Course',
|
|
227
|
+
...userConfig,
|
|
228
|
+
navigation: { mode: 'free', ...userConfig.navigation },
|
|
229
|
+
completion: { mode: 'percentage', percentageThreshold: 100, ...userConfig.completion },
|
|
230
|
+
scoring: { passingScore: 70, ...userConfig.scoring },
|
|
231
|
+
export: { standard: 'web', ...userConfig.export },
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
return `export default ${JSON.stringify(merged)};`;
|
|
235
|
+
}
|
|
236
|
+
return null;
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ---------- Manifest Watch Helpers ----------
|
|
242
|
+
|
|
243
|
+
/** Register all _meta.js and .svelte files under pagesDir as watch files for build mode. */
|
|
244
|
+
function addWatchFiles(ctx: { addWatchFile(id: string): void }, dir: string): void {
|
|
245
|
+
if (!existsSync(dir)) return;
|
|
246
|
+
for (const entry of readdirSync(dir)) {
|
|
247
|
+
const full = resolve(dir, entry);
|
|
248
|
+
if (statSync(full).isDirectory()) {
|
|
249
|
+
addWatchFiles(ctx, full);
|
|
250
|
+
} else if (entry.endsWith('.svelte') || entry === '_meta.js') {
|
|
251
|
+
ctx.addWatchFile(full);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ---------- Pages Plugin ----------
|
|
257
|
+
|
|
258
|
+
const VIRTUAL_PAGES_ID = 'virtual:tessera-pages';
|
|
259
|
+
const RESOLVED_PAGES_ID = '\0' + VIRTUAL_PAGES_ID;
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Provides a virtual module that exports an import.meta.glob map for all .svelte
|
|
263
|
+
* pages. This runs in the user's project context so the glob resolves against their
|
|
264
|
+
* pages/ directory, and Vite can statically analyze it for code splitting.
|
|
265
|
+
*/
|
|
266
|
+
function tesseraPagesPlugin(): Plugin {
|
|
267
|
+
return {
|
|
268
|
+
name: 'tessera:pages',
|
|
269
|
+
enforce: 'pre',
|
|
270
|
+
|
|
271
|
+
resolveId(id) {
|
|
272
|
+
if (id === VIRTUAL_PAGES_ID) return RESOLVED_PAGES_ID;
|
|
273
|
+
return null;
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
load(id) {
|
|
277
|
+
if (id === RESOLVED_PAGES_ID) {
|
|
278
|
+
return `export default import.meta.glob('/pages/**/*.svelte');`;
|
|
279
|
+
}
|
|
280
|
+
return null;
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ---------- Validation Plugin ----------
|
|
286
|
+
|
|
287
|
+
function tesseraValidationPlugin(): Plugin {
|
|
288
|
+
let projectRoot: string;
|
|
289
|
+
let isBuild = false;
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
name: 'tessera:validation',
|
|
293
|
+
enforce: 'pre',
|
|
294
|
+
|
|
295
|
+
configResolved(config: ResolvedConfig) {
|
|
296
|
+
projectRoot = config.root;
|
|
297
|
+
isBuild = config.command === 'build';
|
|
298
|
+
// Run validation during dev (configResolved fires before server starts)
|
|
299
|
+
if (!isBuild) {
|
|
300
|
+
runValidation(projectRoot);
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
|
|
304
|
+
buildStart() {
|
|
305
|
+
// Run validation during build (buildStart fires once before bundling)
|
|
306
|
+
if (isBuild) {
|
|
307
|
+
runValidation(projectRoot);
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function runValidation(projectRoot: string): void {
|
|
314
|
+
const { errors, warnings } = validateProject(projectRoot);
|
|
315
|
+
|
|
316
|
+
for (const warning of warnings) {
|
|
317
|
+
console.warn(`\x1b[33m[tessera warning]\x1b[0m ${warning}`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (errors.length > 0) {
|
|
321
|
+
for (const error of errors) {
|
|
322
|
+
console.error(`\x1b[31m[tessera error]\x1b[0m ${error}`);
|
|
323
|
+
}
|
|
324
|
+
throw new Error(
|
|
325
|
+
`Tessera validation failed with ${errors.length} error(s). Fix the errors above to continue.`
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ---------- Export Plugin ----------
|
|
331
|
+
|
|
332
|
+
function tesseraExportPlugin(): Plugin {
|
|
333
|
+
let projectRoot: string;
|
|
334
|
+
let isBuild = false;
|
|
335
|
+
|
|
336
|
+
return {
|
|
337
|
+
name: 'tessera:export',
|
|
338
|
+
enforce: 'post',
|
|
339
|
+
|
|
340
|
+
configResolved(config: ResolvedConfig) {
|
|
341
|
+
projectRoot = config.root;
|
|
342
|
+
isBuild = config.command === 'build';
|
|
343
|
+
},
|
|
344
|
+
|
|
345
|
+
async closeBundle() {
|
|
346
|
+
if (!isBuild) return;
|
|
347
|
+
|
|
348
|
+
const configPath = resolve(projectRoot, 'course.config.js');
|
|
349
|
+
if (!existsSync(configPath)) {
|
|
350
|
+
// Validation already required course.config.js — getting here means
|
|
351
|
+
// the file vanished mid-build. Surface that loudly rather than
|
|
352
|
+
// shipping a bundle with no LMS export silently.
|
|
353
|
+
throw new Error(
|
|
354
|
+
'[tessera:export] course.config.js not found at closeBundle. The file must exist for the export step to run.'
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const objectStr = extractDefaultExportObjectLiteral(readFileSync(configPath, 'utf-8'));
|
|
359
|
+
if (!objectStr) {
|
|
360
|
+
throw new Error(
|
|
361
|
+
'[tessera:export] course.config.js: could not locate `export default { ... }`. Cannot determine export.standard.'
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
let config: any;
|
|
366
|
+
try {
|
|
367
|
+
config = JSON5.parse(objectStr);
|
|
368
|
+
} catch (err) {
|
|
369
|
+
throw new Error(
|
|
370
|
+
`[tessera:export] course.config.js: failed to parse export-default object literal — ${(err as Error).message}`
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
await runExport(projectRoot, config);
|
|
375
|
+
},
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// ---------- Manifest Plugin ----------
|
|
380
|
+
|
|
381
|
+
const VIRTUAL_MANIFEST_ID = 'virtual:tessera-manifest';
|
|
382
|
+
const RESOLVED_MANIFEST_ID = '\0' + VIRTUAL_MANIFEST_ID;
|
|
383
|
+
|
|
384
|
+
function tesseraManifestPlugin(): Plugin {
|
|
385
|
+
let projectRoot: string;
|
|
386
|
+
let pagesDir: string;
|
|
387
|
+
let currentManifest: Manifest | null = null;
|
|
388
|
+
let server: ViteDevServer | null = null;
|
|
389
|
+
|
|
390
|
+
function buildManifest(): Manifest {
|
|
391
|
+
currentManifest = generateManifest(pagesDir);
|
|
392
|
+
return currentManifest;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return {
|
|
396
|
+
name: 'tessera:manifest',
|
|
397
|
+
enforce: 'pre',
|
|
398
|
+
|
|
399
|
+
configResolved(config: ResolvedConfig) {
|
|
400
|
+
projectRoot = config.root;
|
|
401
|
+
pagesDir = resolve(projectRoot, 'pages');
|
|
402
|
+
},
|
|
403
|
+
|
|
404
|
+
configureServer(devServer: ViteDevServer) {
|
|
405
|
+
server = devServer;
|
|
406
|
+
|
|
407
|
+
// Watch the pages directory for changes
|
|
408
|
+
devServer.watcher.on('all', (event, filePath) => {
|
|
409
|
+
if (!filePath.startsWith(pagesDir)) return;
|
|
410
|
+
|
|
411
|
+
// Rebuild manifest on relevant file changes
|
|
412
|
+
const isRelevant =
|
|
413
|
+
filePath.endsWith('.svelte') ||
|
|
414
|
+
filePath.endsWith('_meta.js') ||
|
|
415
|
+
event === 'addDir' ||
|
|
416
|
+
event === 'unlinkDir';
|
|
417
|
+
|
|
418
|
+
if (isRelevant) {
|
|
419
|
+
currentManifest = null; // invalidate cache
|
|
420
|
+
|
|
421
|
+
// Invalidate the virtual module to trigger HMR
|
|
422
|
+
const mod = devServer.moduleGraph.getModuleById(RESOLVED_MANIFEST_ID);
|
|
423
|
+
if (mod) {
|
|
424
|
+
devServer.moduleGraph.invalidateModule(mod);
|
|
425
|
+
devServer.ws.send({ type: 'full-reload' });
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
console.log(`[tessera] Manifest rebuilt (${event}: ${filePath.replace(projectRoot, '')})`);
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
},
|
|
432
|
+
|
|
433
|
+
buildStart() {
|
|
434
|
+
buildManifest();
|
|
435
|
+
},
|
|
436
|
+
|
|
437
|
+
resolveId(id) {
|
|
438
|
+
if (id === VIRTUAL_MANIFEST_ID) return RESOLVED_MANIFEST_ID;
|
|
439
|
+
return null;
|
|
440
|
+
},
|
|
441
|
+
|
|
442
|
+
load(id) {
|
|
443
|
+
if (id === RESOLVED_MANIFEST_ID) {
|
|
444
|
+
if (!currentManifest) {
|
|
445
|
+
buildManifest();
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Register watch files so Vite's built-in watcher (used in build --watch)
|
|
449
|
+
// knows to re-trigger when pages/ content changes.
|
|
450
|
+
addWatchFiles(this, pagesDir);
|
|
451
|
+
|
|
452
|
+
// Encode as base64 to prevent Vite's import analysis from
|
|
453
|
+
// scanning .svelte importPath strings as module imports.
|
|
454
|
+
// Replace Infinity with 1e9 since JSON.stringify drops it.
|
|
455
|
+
const json = JSON.stringify(currentManifest, (_key, value) =>
|
|
456
|
+
value === Infinity ? 1e9 : value
|
|
457
|
+
);
|
|
458
|
+
const b64 = Buffer.from(json).toString('base64');
|
|
459
|
+
return `export default JSON.parse(atob("${b64}"));`;
|
|
460
|
+
}
|
|
461
|
+
return null;
|
|
462
|
+
},
|
|
463
|
+
};
|
|
464
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Plugin, ResolvedConfig, ViteDevServer } from 'vite';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const VIRTUAL_LAYOUT_ID = 'virtual:tessera-layout';
|
|
6
|
+
const RESOLVED_LAYOUT_ID = '\0' + VIRTUAL_LAYOUT_ID;
|
|
7
|
+
|
|
8
|
+
export function tesseraLayoutPlugin(): Plugin {
|
|
9
|
+
let projectRoot: string;
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
name: 'tessera:layout',
|
|
13
|
+
enforce: 'pre',
|
|
14
|
+
|
|
15
|
+
configResolved(config: ResolvedConfig) {
|
|
16
|
+
projectRoot = config.root;
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
resolveId(id) {
|
|
20
|
+
if (id === VIRTUAL_LAYOUT_ID) return RESOLVED_LAYOUT_ID;
|
|
21
|
+
return null;
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
load(id) {
|
|
25
|
+
if (id !== RESOLVED_LAYOUT_ID) return null;
|
|
26
|
+
const layoutPath = resolve(projectRoot, 'layout.svelte');
|
|
27
|
+
if (existsSync(layoutPath)) {
|
|
28
|
+
// Register the file with Vite so edits trigger HMR / build --watch
|
|
29
|
+
// re-runs. Only add when the file actually exists — calling
|
|
30
|
+
// addWatchFile on a non-existent path makes Vite's importAnalysis
|
|
31
|
+
// try to resolve it as a real import.
|
|
32
|
+
this.addWatchFile(layoutPath);
|
|
33
|
+
const normalized = layoutPath.replace(/\\/g, '/');
|
|
34
|
+
return `export { default } from '${normalized}';`;
|
|
35
|
+
}
|
|
36
|
+
return `export default null;`;
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
configureServer(server: ViteDevServer) {
|
|
40
|
+
const layoutPath = resolve(projectRoot, 'layout.svelte');
|
|
41
|
+
// Only react to add/unlink: those flip the virtual module's load() output
|
|
42
|
+
// between `export default null` and `export { default } from '...'`. A
|
|
43
|
+
// `change` event leaves that output identical and is handled by Svelte's
|
|
44
|
+
// own HMR for the underlying file — full-reloading on every edit would
|
|
45
|
+
// wipe in-page state for no reason.
|
|
46
|
+
server.watcher.on('all', (event, filePath) => {
|
|
47
|
+
if (filePath !== layoutPath) return;
|
|
48
|
+
if (event !== 'add' && event !== 'unlink') return;
|
|
49
|
+
const mod = server.moduleGraph.getModuleById(RESOLVED_LAYOUT_ID);
|
|
50
|
+
if (mod) server.moduleGraph.invalidateModule(mod);
|
|
51
|
+
server.ws.send({ type: 'full-reload' });
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|