@hutusi/amytis 1.14.0 → 1.16.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/.github/workflows/ci.yml +1 -1
- package/.github/workflows/publish.yml +2 -2
- package/CHANGELOG.md +42 -0
- package/CLAUDE.md +90 -219
- package/README.md +33 -1
- package/README.zh.md +33 -1
- package/TODO.md +10 -0
- package/bun.lock +205 -539
- package/content/books/sample-book/index.mdx +3 -0
- package/content/posts/code-block-features-showcase.mdx +223 -0
- package/content/series/rst-legacy/deeper-notes/images/test.svg +4 -0
- package/content/series/rst-legacy/deeper-notes/index.rst +15 -0
- package/content/series/rst-legacy/getting-started.rst +24 -0
- package/content/series/rst-legacy/index.rst +9 -0
- package/content/series/rst-readme/README.rst +9 -0
- package/content/series/rst-readme/readme-index-post.rst +10 -0
- package/content/series/rst-toctree/first-post.rst +6 -0
- package/content/series/rst-toctree/index.rst +10 -0
- package/content/series/rst-toctree/second-post.rst +6 -0
- package/content/series/rst-toctree-precedence/first-post.rst +6 -0
- package/content/series/rst-toctree-precedence/index.rst +12 -0
- package/content/series/rst-toctree-precedence/second-post.rst +6 -0
- package/docs/ALERTS.md +112 -0
- package/docs/ARCHITECTURE.md +239 -8
- package/docs/CODE-BLOCKS.md +238 -0
- package/docs/CONTRIBUTING.md +36 -0
- package/docs/guides/README.md +11 -0
- package/docs/guides/importing-vuepress-books.md +178 -0
- package/eslint.config.mjs +20 -6
- package/next.config.ts +2 -2
- package/package.json +52 -24
- package/packages/create-amytis/package.json +1 -1
- package/packages/create-amytis/src/index.test.ts +43 -1
- package/packages/create-amytis/src/index.ts +64 -8
- package/public/next-image-export-optimizer-hashes.json +14 -73
- package/scripts/build-pagefind.ts +172 -0
- package/scripts/copy-assets.ts +246 -56
- package/scripts/generate-code-group-icons.ts +79 -0
- package/scripts/generate-knowledge-graph.ts +2 -1
- package/scripts/render-rst.py +923 -0
- package/scripts/run-with-rst-python.ts +42 -0
- package/scripts/sync-vuepress-book.ts +499 -0
- package/src/app/[slug]/[postSlug]/page.tsx +20 -10
- package/src/app/[slug]/page/[page]/page.tsx +15 -0
- package/src/app/books/[slug]/{[chapter] → [...chapter]}/page.tsx +32 -10
- package/src/app/books/[slug]/page.tsx +67 -32
- package/src/app/globals.css +639 -94
- package/src/app/page.tsx +1 -1
- package/src/app/series/[slug]/page/[page]/page.tsx +74 -6
- package/src/app/series/[slug]/page.tsx +11 -13
- package/src/app/series/page.tsx +3 -3
- package/src/app/sitemap.ts +3 -3
- package/src/components/ArticleCopyCleaner.tsx +64 -0
- package/src/components/AuthorCard.tsx +25 -16
- package/src/components/BookMobileNav.tsx +44 -50
- package/src/components/BookSidebar.tsx +0 -0
- package/src/components/CodeBlock.test.tsx +93 -8
- package/src/components/CodeBlock.tsx +39 -101
- package/src/components/CodeBlockToolbar.tsx +88 -0
- package/src/components/CodeGroup.tsx +81 -0
- package/src/components/CoverImage.tsx +6 -2
- package/src/components/ExternalLinkIcon.tsx +15 -0
- package/src/components/FeaturedStoriesSection.tsx +3 -3
- package/src/components/GithubAlert.tsx +97 -0
- package/src/components/MarkdownRenderer.test.tsx +30 -4
- package/src/components/MarkdownRenderer.tsx +148 -24
- package/src/components/Mermaid.tsx +32 -1
- package/src/components/PostList.tsx +1 -1
- package/src/components/PostNavigation.tsx +13 -2
- package/src/components/PostSidebar.tsx +13 -2
- package/src/components/RstRenderer.test.tsx +93 -0
- package/src/components/RstRenderer.tsx +157 -0
- package/src/components/Search.tsx +18 -4
- package/src/components/SeriesCatalog.tsx +1 -1
- package/src/components/ShareBar.tsx +5 -0
- package/src/components/TocPanel.tsx +10 -2
- package/src/i18n/translations.ts +2 -0
- package/src/layouts/BookLayout.tsx +35 -4
- package/src/layouts/PostLayout.tsx +10 -2
- package/src/layouts/SimpleLayout.tsx +10 -3
- package/src/lib/code-group-icons.test.ts +78 -0
- package/src/lib/code-group-icons.ts +148 -0
- package/src/lib/image-utils.test.ts +19 -0
- package/src/lib/image-utils.ts +11 -0
- package/src/lib/markdown.test.ts +195 -14
- package/src/lib/markdown.ts +928 -254
- package/src/lib/normalize-vuepress-math.ts +118 -0
- package/src/lib/rehype-fence-meta.ts +22 -0
- package/src/lib/rehype-image-metadata.ts +2 -2
- package/src/lib/remark-book-chapter-links.ts +106 -0
- package/src/lib/remark-code-group.ts +54 -0
- package/src/lib/remark-github-alerts.test.ts +83 -0
- package/src/lib/remark-github-alerts.ts +65 -0
- package/src/lib/remark-vuepress-containers.ts +130 -0
- package/src/lib/rst-renderer.test.ts +355 -0
- package/src/lib/rst-renderer.ts +629 -0
- package/src/lib/rst.test.ts +350 -0
- package/src/lib/rst.ts +674 -0
- package/src/lib/series-redirects.ts +42 -0
- package/src/lib/shiki-rst.ts +185 -0
- package/src/lib/shiki.test.ts +153 -0
- package/src/lib/shiki.ts +292 -0
- package/src/lib/urls.ts +57 -0
- package/src/test-utils/render.ts +23 -0
- package/tests/fixtures/sync-vuepress-book/docs/.vuepress/config.js +43 -0
- package/tests/fixtures/sync-vuepress-book/docs/intro/welcome.md +7 -0
- package/tests/fixtures/sync-vuepress-book/docs/maths/linear/assets/diagram.png +1 -0
- package/tests/fixtures/sync-vuepress-book/docs/maths/linear/matrices.md +7 -0
- package/tests/fixtures/sync-vuepress-book/docs/maths/linear/vectors.md +9 -0
- package/tests/helpers/env.ts +19 -0
- package/tests/integration/book-chapter-links.test.ts +107 -0
- package/tests/integration/books-nested-toc.test.ts +176 -0
- package/tests/integration/books.test.ts +3 -2
- package/tests/integration/code-block-features.test.ts +188 -0
- package/tests/integration/code-group.test.ts +183 -0
- package/tests/integration/code-notation.test.ts +97 -0
- package/tests/integration/feed-utils.test.ts +13 -0
- package/tests/integration/github-alerts.test.ts +82 -0
- package/tests/integration/markdown-external-links.test.ts +103 -0
- package/tests/integration/normalize-vuepress-math.test.ts +149 -0
- package/tests/integration/reading-time-headings.test.ts +12 -14
- package/tests/integration/series-draft.test.ts +12 -5
- package/tests/integration/series.test.ts +93 -0
- package/tests/integration/sync-vuepress-book.test.ts +240 -0
- package/tests/integration/vuepress-containers.test.ts +107 -0
- package/tests/tooling/build-pagefind.test.ts +66 -0
- package/tests/tooling/new-post.test.ts +1 -1
- package/tests/unit/static-params.test.ts +166 -13
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* • afterAll restores the real module so any subsequent tests see real data.
|
|
22
22
|
*/
|
|
23
23
|
import { describe, test, expect, mock, beforeAll, beforeEach, afterAll, afterEach } from 'bun:test';
|
|
24
|
+
import { setEnvVar, restoreEnvVar } from '../helpers/env';
|
|
24
25
|
|
|
25
26
|
// ─── Capture real modules ─────────────────────────────────────────────────────
|
|
26
27
|
// Static imports are hoisted and resolved before any executable code (including
|
|
@@ -36,7 +37,18 @@ import * as realUrls from '../../src/lib/urls';
|
|
|
36
37
|
// reference, but they are never mutated during tests so this is safe.
|
|
37
38
|
const snapshotUrls = { ...realUrls };
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
// Mock-post shape: only `slug` is required; the named fields are the ones
|
|
41
|
+
// production code paths read. Extra fields (full Post shape) are allowed via
|
|
42
|
+
// the index signature so individual tests can pass realistic fixtures without
|
|
43
|
+
// every property having to be enumerated here.
|
|
44
|
+
let mockedPosts: Array<{
|
|
45
|
+
slug: string;
|
|
46
|
+
series?: string;
|
|
47
|
+
redirectFrom?: string[];
|
|
48
|
+
draft?: boolean;
|
|
49
|
+
title?: string;
|
|
50
|
+
[key: string]: unknown;
|
|
51
|
+
}> = [];
|
|
40
52
|
let mockedNotes: Array<{ slug: string }> = [];
|
|
41
53
|
let mockedSeries: Record<string, Array<{ slug: string }>> = {};
|
|
42
54
|
let mockedSeriesData: Record<string, { redirectFrom?: string[]; title?: string }> = {};
|
|
@@ -131,6 +143,7 @@ beforeAll(() => {
|
|
|
131
143
|
getSeriesData: (slug: string) => mockedSeriesData[slug] ?? null,
|
|
132
144
|
getSeriesPosts: () => [],
|
|
133
145
|
getSeriesAuthors: () => [],
|
|
146
|
+
getCollectionsForPost: () => [],
|
|
134
147
|
|
|
135
148
|
getAuthorSlug: (name: string) =>
|
|
136
149
|
name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''),
|
|
@@ -147,7 +160,7 @@ beforeEach(() => {
|
|
|
147
160
|
mockedNotes = [];
|
|
148
161
|
mockedSeries = {};
|
|
149
162
|
mockedSeriesData = {};
|
|
150
|
-
|
|
163
|
+
restoreEnvVar('NODE_ENV', originalNodeEnv);
|
|
151
164
|
});
|
|
152
165
|
|
|
153
166
|
afterEach(() => {
|
|
@@ -155,7 +168,7 @@ afterEach(() => {
|
|
|
155
168
|
mockedNotes = [];
|
|
156
169
|
mockedSeries = {};
|
|
157
170
|
mockedSeriesData = {};
|
|
158
|
-
|
|
171
|
+
restoreEnvVar('NODE_ENV', originalNodeEnv);
|
|
159
172
|
});
|
|
160
173
|
|
|
161
174
|
// ─── Restore real modules ─────────────────────────────────────────────────────
|
|
@@ -200,7 +213,7 @@ describe('generateStaticParams — placeholder when content is empty', () => {
|
|
|
200
213
|
|
|
201
214
|
test('notes/[slug] includes raw and encoded Unicode slug in non-production', async () => {
|
|
202
215
|
mockedNotes = [{ slug: '推理模型' }];
|
|
203
|
-
|
|
216
|
+
setEnvVar('NODE_ENV', 'development');
|
|
204
217
|
const { generateStaticParams } = await import('../../src/app/notes/[slug]/page');
|
|
205
218
|
const params = generateStaticParams();
|
|
206
219
|
expect(params).toContainEqual({ slug: '推理模型' });
|
|
@@ -209,7 +222,7 @@ describe('generateStaticParams — placeholder when content is empty', () => {
|
|
|
209
222
|
|
|
210
223
|
test('notes/[slug] includes only raw Unicode slug in production', async () => {
|
|
211
224
|
mockedNotes = [{ slug: '推理模型' }];
|
|
212
|
-
|
|
225
|
+
setEnvVar('NODE_ENV', 'production');
|
|
213
226
|
const { generateStaticParams } = await import('../../src/app/notes/[slug]/page');
|
|
214
227
|
const params = generateStaticParams();
|
|
215
228
|
expect(params).toContainEqual({ slug: '推理模型' });
|
|
@@ -231,10 +244,10 @@ describe('generateStaticParams — placeholder when content is empty', () => {
|
|
|
231
244
|
expect(params).toEqual([{ slug: '_' }]);
|
|
232
245
|
});
|
|
233
246
|
|
|
234
|
-
test('books/[slug]/[chapter] returns [{ slug: "_", chapter: "_" }]', async () => {
|
|
235
|
-
const { generateStaticParams } = await import('../../src/app/books/[slug]/[chapter]/page');
|
|
247
|
+
test('books/[slug]/[...chapter] returns [{ slug: "_", chapter: ["_"] }]', async () => {
|
|
248
|
+
const { generateStaticParams } = await import('../../src/app/books/[slug]/[...chapter]/page');
|
|
236
249
|
const params = await generateStaticParams();
|
|
237
|
-
expect(params).toEqual([{ slug: '_', chapter: '_' }]);
|
|
250
|
+
expect(params).toEqual([{ slug: '_', chapter: ['_'] }]);
|
|
238
251
|
});
|
|
239
252
|
});
|
|
240
253
|
|
|
@@ -254,11 +267,101 @@ describe('generateStaticParams — placeholder when content is empty', () => {
|
|
|
254
267
|
expect(params).toContainEqual({ slug: 'old-name' });
|
|
255
268
|
});
|
|
256
269
|
|
|
270
|
+
test('series/[slug] includes raw and encoded Unicode slug in non-production', async () => {
|
|
271
|
+
mockedSeries = { '软件构架设计': [] };
|
|
272
|
+
setEnvVar('NODE_ENV', 'development');
|
|
273
|
+
const { generateStaticParams } = await import('../../src/app/series/[slug]/page');
|
|
274
|
+
const params = await generateStaticParams();
|
|
275
|
+
expect(params).toContainEqual({ slug: '软件构架设计' });
|
|
276
|
+
expect(params).toContainEqual({ slug: '%E8%BD%AF%E4%BB%B6%E6%9E%84%E6%9E%B6%E8%AE%BE%E8%AE%A1' });
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test('series/[slug] includes only raw Unicode slug in production', async () => {
|
|
280
|
+
mockedSeries = { '软件构架设计': [] };
|
|
281
|
+
setEnvVar('NODE_ENV', 'production');
|
|
282
|
+
const { generateStaticParams } = await import('../../src/app/series/[slug]/page');
|
|
283
|
+
const params = await generateStaticParams();
|
|
284
|
+
expect(params).toContainEqual({ slug: '软件构架设计' });
|
|
285
|
+
expect(params).not.toContainEqual({ slug: '%E8%BD%AF%E4%BB%B6%E6%9E%84%E6%9E%B6%E8%AE%BE%E8%AE%A1' });
|
|
286
|
+
});
|
|
287
|
+
|
|
257
288
|
test('series/[slug]/page/[page] returns [{ slug: "_", page: "2" }]', async () => {
|
|
258
289
|
const { generateStaticParams } = await import('../../src/app/series/[slug]/page/[page]/page');
|
|
259
290
|
const params = await generateStaticParams();
|
|
260
291
|
expect(params).toEqual([{ slug: '_', page: '2' }]);
|
|
261
292
|
});
|
|
293
|
+
|
|
294
|
+
test('series/[slug]/page/[page] includes encoded Unicode slug in non-production', async () => {
|
|
295
|
+
mockedSeries = { '软件构架设计': Array.from({ length: 6 }, (_, i) => ({ slug: `p${i + 1}` })) };
|
|
296
|
+
setEnvVar('NODE_ENV', 'development');
|
|
297
|
+
const { generateStaticParams } = await import('../../src/app/series/[slug]/page/[page]/page');
|
|
298
|
+
const params = await generateStaticParams();
|
|
299
|
+
expect(params).toContainEqual({ slug: '软件构架设计', page: '2' });
|
|
300
|
+
expect(params).toContainEqual({ slug: '%E8%BD%AF%E4%BB%B6%E6%9E%84%E6%9E%B6%E8%AE%BE%E8%AE%A1', page: '2' });
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test('series/[slug]/page/[page] includes redirectFrom slug when series is renamed', async () => {
|
|
304
|
+
mockedSeries = { 'new-name': Array.from({ length: 6 }, (_, i) => ({ slug: `p${i + 1}` })) };
|
|
305
|
+
mockedSeriesData = { 'new-name': { redirectFrom: ['/series/old-name'], title: 'New Series' } };
|
|
306
|
+
const { generateStaticParams } = await import('../../src/app/series/[slug]/page/[page]/page');
|
|
307
|
+
const params = await generateStaticParams();
|
|
308
|
+
expect(params).toContainEqual({ slug: 'new-name', page: '2' });
|
|
309
|
+
expect(params).toContainEqual({ slug: 'old-name', page: '2' });
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
test('series/[slug]/page/[page] redirects old alias slugs to the canonical paginated path', async () => {
|
|
313
|
+
mockedSeries = { 'new-name': Array.from({ length: 6 }, (_, i) => ({ slug: `p${i + 1}` })) };
|
|
314
|
+
mockedSeriesData = { 'new-name': { redirectFrom: ['/series/old-name'], title: 'New Series' } };
|
|
315
|
+
const page = await import('../../src/app/series/[slug]/page/[page]/page');
|
|
316
|
+
await expect(page.default({
|
|
317
|
+
params: Promise.resolve({ slug: 'old-name', page: '2' }),
|
|
318
|
+
})).resolves.toBeDefined();
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test('series routes match percent-encoded redirectFrom aliases after normalization', async () => {
|
|
322
|
+
mockedSeries = { '软件构架设计': Array.from({ length: 6 }, (_, i) => ({ slug: `p${i + 1}` })) };
|
|
323
|
+
mockedSeriesData = {
|
|
324
|
+
'软件构架设计': {
|
|
325
|
+
redirectFrom: ['/series/%E8%BD%AF%E4%BB%B6%E8%AE%BE%E8%AE%A1'],
|
|
326
|
+
title: '软件构架设计',
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const seriesPage = await import('../../src/app/series/[slug]/page');
|
|
331
|
+
await expect(seriesPage.default({
|
|
332
|
+
params: Promise.resolve({ slug: '%E8%BD%AF%E4%BB%B6%E8%AE%BE%E8%AE%A1' }),
|
|
333
|
+
})).resolves.toBeDefined();
|
|
334
|
+
|
|
335
|
+
const paginatedPage = await import('../../src/app/series/[slug]/page/[page]/page');
|
|
336
|
+
await expect(paginatedPage.default({
|
|
337
|
+
params: Promise.resolve({ slug: '%E8%BD%AF%E4%BB%B6%E8%AE%BE%E8%AE%A1', page: '2' }),
|
|
338
|
+
})).resolves.toBeDefined();
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test('series/[slug]/page/[page] throws when redirectFrom alias conflicts with an existing series slug', async () => {
|
|
342
|
+
mockedSeries = {
|
|
343
|
+
'existing-slug': Array.from({ length: 6 }, (_, i) => ({ slug: `a${i + 1}` })),
|
|
344
|
+
'new-name': Array.from({ length: 6 }, (_, i) => ({ slug: `b${i + 1}` })),
|
|
345
|
+
};
|
|
346
|
+
mockedSeriesData = {
|
|
347
|
+
'new-name': { redirectFrom: ['/series/existing-slug'], title: 'New Series' },
|
|
348
|
+
};
|
|
349
|
+
const { generateStaticParams } = await import('../../src/app/series/[slug]/page/[page]/page');
|
|
350
|
+
await expect(generateStaticParams()).rejects.toThrow(/conflicts with an existing series slug/i);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
test('series/[slug]/page/[page] throws when two series claim the same redirectFrom alias', async () => {
|
|
354
|
+
mockedSeries = {
|
|
355
|
+
'first-series': Array.from({ length: 6 }, (_, i) => ({ slug: `a${i + 1}` })),
|
|
356
|
+
'second-series': Array.from({ length: 6 }, (_, i) => ({ slug: `b${i + 1}` })),
|
|
357
|
+
};
|
|
358
|
+
mockedSeriesData = {
|
|
359
|
+
'first-series': { redirectFrom: ['/series/old-name'], title: 'First' },
|
|
360
|
+
'second-series': { redirectFrom: ['/series/old-name'], title: 'Second' },
|
|
361
|
+
};
|
|
362
|
+
const { generateStaticParams } = await import('../../src/app/series/[slug]/page/[page]/page');
|
|
363
|
+
await expect(generateStaticParams()).rejects.toThrow(/claimed by both/i);
|
|
364
|
+
});
|
|
262
365
|
});
|
|
263
366
|
|
|
264
367
|
describe('posts routes', () => {
|
|
@@ -270,7 +373,7 @@ describe('generateStaticParams — placeholder when content is empty', () => {
|
|
|
270
373
|
|
|
271
374
|
test('posts/[slug] includes raw and encoded Unicode slug in non-production', async () => {
|
|
272
375
|
mockedPosts = [{ slug: '中文测试文章' }];
|
|
273
|
-
|
|
376
|
+
setEnvVar('NODE_ENV', 'development');
|
|
274
377
|
const { generateStaticParams } = await import('../../src/app/posts/[slug]/page');
|
|
275
378
|
const params = await generateStaticParams();
|
|
276
379
|
|
|
@@ -280,7 +383,7 @@ describe('generateStaticParams — placeholder when content is empty', () => {
|
|
|
280
383
|
|
|
281
384
|
test('posts/[slug] includes only raw Unicode slug in production', async () => {
|
|
282
385
|
mockedPosts = [{ slug: '中文测试文章' }];
|
|
283
|
-
|
|
386
|
+
setEnvVar('NODE_ENV', 'production');
|
|
284
387
|
const { generateStaticParams } = await import('../../src/app/posts/[slug]/page');
|
|
285
388
|
const params = await generateStaticParams();
|
|
286
389
|
|
|
@@ -394,7 +497,7 @@ describe('generateStaticParams — placeholder when content is empty', () => {
|
|
|
394
497
|
|
|
395
498
|
test('[slug]/page does not include single-segment redirectFrom for draft posts in production', async () => {
|
|
396
499
|
mockedPosts = [{ slug: 'my-post', draft: true, redirectFrom: ['/old-slug'] }];
|
|
397
|
-
|
|
500
|
+
setEnvVar('NODE_ENV', 'production');
|
|
398
501
|
const { generateStaticParams } = await import('../../src/app/[slug]/page');
|
|
399
502
|
const params = await generateStaticParams();
|
|
400
503
|
expect(params).not.toContainEqual({ slug: 'old-slug' });
|
|
@@ -497,22 +600,72 @@ describe('generateStaticParams — placeholder when content is empty', () => {
|
|
|
497
600
|
test('[slug]/[postSlug] includes encoded Unicode postSlug variants in non-production', async () => {
|
|
498
601
|
// Use redirectFrom to place a Unicode postSlug at a 2-segment path — no url mock needed.
|
|
499
602
|
mockedPosts = [{ slug: 'my-post', redirectFrom: ['/old-prefix/中文文章'] }];
|
|
500
|
-
|
|
603
|
+
setEnvVar('NODE_ENV', 'development');
|
|
501
604
|
const { generateStaticParams } = await import('../../src/app/[slug]/[postSlug]/page');
|
|
502
605
|
const params = await generateStaticParams();
|
|
503
606
|
expect(params).toContainEqual({ slug: 'old-prefix', postSlug: '中文文章' });
|
|
504
607
|
expect(params).toContainEqual({ slug: 'old-prefix', postSlug: encodeURIComponent('中文文章') });
|
|
505
608
|
});
|
|
506
609
|
|
|
610
|
+
test('[slug]/[postSlug] includes encoded Unicode prefix variants in non-production', async () => {
|
|
611
|
+
mockedSeries = { '软件构架设计': [{ slug: 'architecture-post' }] };
|
|
612
|
+
setEnvVar('NODE_ENV', 'development');
|
|
613
|
+
const { generateStaticParams } = await import('../../src/app/[slug]/[postSlug]/page');
|
|
614
|
+
const params = await generateStaticParams();
|
|
615
|
+
expect(params).toContainEqual({ slug: '软件构架设计', postSlug: 'architecture-post' });
|
|
616
|
+
expect(params).toContainEqual({ slug: encodeURIComponent('软件构架设计'), postSlug: 'architecture-post' });
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
test('[slug]/[postSlug] includes encoded Unicode prefix and postSlug variants together in non-production', async () => {
|
|
620
|
+
mockedSeries = { '软件构架设计': [{ slug: '中文文章' }] };
|
|
621
|
+
setEnvVar('NODE_ENV', 'development');
|
|
622
|
+
const { generateStaticParams } = await import('../../src/app/[slug]/[postSlug]/page');
|
|
623
|
+
const params = await generateStaticParams();
|
|
624
|
+
expect(params).toContainEqual({ slug: '软件构架设计', postSlug: '中文文章' });
|
|
625
|
+
expect(params).toContainEqual({ slug: encodeURIComponent('软件构架设计'), postSlug: '中文文章' });
|
|
626
|
+
expect(params).toContainEqual({ slug: '软件构架设计', postSlug: encodeURIComponent('中文文章') });
|
|
627
|
+
expect(params).toContainEqual({ slug: encodeURIComponent('软件构架设计'), postSlug: encodeURIComponent('中文文章') });
|
|
628
|
+
});
|
|
629
|
+
|
|
507
630
|
test('[slug]/[postSlug] does not include encoded Unicode postSlug variants in production', async () => {
|
|
508
631
|
mockedPosts = [{ slug: 'my-post', redirectFrom: ['/old-prefix/中文文章'] }];
|
|
509
|
-
|
|
632
|
+
setEnvVar('NODE_ENV', 'production');
|
|
510
633
|
const { generateStaticParams } = await import('../../src/app/[slug]/[postSlug]/page');
|
|
511
634
|
const params = await generateStaticParams();
|
|
512
635
|
expect(params).toContainEqual({ slug: 'old-prefix', postSlug: '中文文章' });
|
|
513
636
|
expect(params).not.toContainEqual({ slug: 'old-prefix', postSlug: encodeURIComponent('中文文章') });
|
|
514
637
|
});
|
|
515
638
|
|
|
639
|
+
test('[slug]/[postSlug] page resolves encoded Unicode series prefix without notFound', async () => {
|
|
640
|
+
mockedSeries = { '软件构架设计': [{ slug: 'my-post' }] };
|
|
641
|
+
mockedSeriesData = { '软件构架设计': { title: '软件构架设计' } };
|
|
642
|
+
mockedPosts = [{
|
|
643
|
+
slug: 'my-post',
|
|
644
|
+
title: 'My Post',
|
|
645
|
+
excerpt: 'Excerpt',
|
|
646
|
+
date: '2026-01-01',
|
|
647
|
+
authors: ['Author'],
|
|
648
|
+
series: '软件构架设计',
|
|
649
|
+
redirectFrom: ['/软件构架设计/my-post'],
|
|
650
|
+
layout: 'post',
|
|
651
|
+
content: 'Body',
|
|
652
|
+
headings: [],
|
|
653
|
+
imageBaseSlug: 'posts',
|
|
654
|
+
category: 'Test',
|
|
655
|
+
tags: [],
|
|
656
|
+
readingMinutes: 1,
|
|
657
|
+
wordCount: 0,
|
|
658
|
+
}];
|
|
659
|
+
|
|
660
|
+
const page = await import('../../src/app/[slug]/[postSlug]/page');
|
|
661
|
+
await expect(page.default({
|
|
662
|
+
params: Promise.resolve({
|
|
663
|
+
slug: encodeURIComponent('软件构架设计'),
|
|
664
|
+
postSlug: 'my-post',
|
|
665
|
+
}),
|
|
666
|
+
})).resolves.toBeDefined();
|
|
667
|
+
});
|
|
668
|
+
|
|
516
669
|
test('[slug]/page/[page]/page returns placeholder when no custom paths', async () => {
|
|
517
670
|
const { generateStaticParams } = await import('../../src/app/[slug]/page/[page]/page');
|
|
518
671
|
const params = await generateStaticParams();
|