@elia97/officina 0.1.0 → 0.3.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.
Files changed (96) hide show
  1. package/README.md +57 -3
  2. package/dist/bin.d.ts +8 -0
  3. package/dist/bin.js +28 -6
  4. package/dist/checks/analytics.d.ts +2 -0
  5. package/dist/checks/analytics.js +48 -0
  6. package/dist/checks/bundle.d.ts +2 -0
  7. package/dist/checks/bundle.js +98 -0
  8. package/dist/checks/comments.d.ts +2 -0
  9. package/dist/checks/doctor.d.ts +2 -0
  10. package/dist/checks/doctor.js +15 -0
  11. package/dist/checks/language.d.ts +2 -0
  12. package/dist/checks/lighthouse.d.ts +2 -0
  13. package/dist/checks/lighthouse.js +37 -0
  14. package/dist/checks/placeholders.d.ts +2 -0
  15. package/dist/checks/roadmap.d.ts +2 -0
  16. package/dist/checks/routes.d.ts +2 -0
  17. package/dist/checks/smoke.d.ts +2 -0
  18. package/dist/checks/smoke.js +57 -0
  19. package/dist/checks/vercel-cli.d.ts +2 -0
  20. package/dist/gen/collection.d.mts +1 -0
  21. package/dist/gen/collection.mjs +76 -0
  22. package/dist/gen/component.d.mts +1 -0
  23. package/dist/gen/component.mjs +41 -0
  24. package/dist/gen/icons.d.ts +2 -0
  25. package/dist/gen/icons.js +27 -0
  26. package/dist/gen/identifier.d.mts +1 -0
  27. package/dist/gen/identifier.mjs +52 -0
  28. package/dist/gen/inject-config.d.mts +12 -0
  29. package/dist/gen/inject-config.mjs +73 -0
  30. package/dist/gen/inject-section.d.mts +16 -0
  31. package/dist/gen/inject-section.mjs +140 -0
  32. package/dist/gen/inject-strings.d.mts +8 -0
  33. package/dist/gen/inject-strings.mjs +56 -0
  34. package/dist/gen/page.d.mts +10 -0
  35. package/dist/gen/page.mjs +68 -0
  36. package/dist/gen/plopfile.d.mts +2 -0
  37. package/dist/gen/plopfile.mjs +30 -0
  38. package/dist/gen/post-gen.d.mts +1 -0
  39. package/dist/gen/post-gen.mjs +16 -0
  40. package/dist/gen/run.d.ts +2 -0
  41. package/dist/gen/run.js +17 -0
  42. package/dist/gen/section-targets.d.mts +23 -0
  43. package/dist/gen/section-targets.mjs +46 -0
  44. package/dist/gen/section.d.mts +1 -0
  45. package/dist/gen/section.mjs +68 -0
  46. package/dist/gen/sectioned-collection.d.mts +26 -0
  47. package/dist/gen/sectioned-collection.mjs +81 -0
  48. package/dist/gen/templates/collection/data.yml.hbs +2 -0
  49. package/dist/gen/templates/collection/doc.md.hbs +8 -0
  50. package/dist/gen/templates/collection/schema.ts.hbs +11 -0
  51. package/dist/gen/templates/component/component.astro.hbs +21 -0
  52. package/dist/gen/templates/page/dynamic.astro.hbs +24 -0
  53. package/dist/gen/templates/page/static.astro.hbs +20 -0
  54. package/dist/gen/templates/section/component.astro.hbs +45 -0
  55. package/dist/gen/templates/section/content.yml.hbs +10 -0
  56. package/dist/gen/templates/section/schema.ts.hbs +20 -0
  57. package/dist/gen/templates/sections/data-layer.ts.hbs +13 -0
  58. package/dist/gen/templates/sections/index.ts.hbs +12 -0
  59. package/dist/gen/templates/sections/page.astro.hbs +30 -0
  60. package/dist/gen/ts-morph-utils.d.mts +1 -0
  61. package/dist/gen/ts-morph-utils.mjs +8 -0
  62. package/dist/index.d.ts +10 -0
  63. package/dist/index.js +5 -0
  64. package/dist/lib/analytics-coverage.d.ts +12 -0
  65. package/dist/lib/analytics-coverage.js +15 -0
  66. package/dist/lib/bundle-budget.d.ts +24 -0
  67. package/dist/lib/bundle-budget.js +50 -0
  68. package/dist/lib/check-comments.d.ts +10 -0
  69. package/dist/lib/check-language.d.ts +11 -0
  70. package/dist/lib/check-placeholders.d.ts +6 -0
  71. package/dist/lib/check-roadmap.d.ts +30 -0
  72. package/dist/lib/check-routes.d.ts +6 -0
  73. package/dist/lib/cli.d.ts +26 -0
  74. package/dist/lib/comment-syntax.d.ts +11 -0
  75. package/dist/lib/config.d.ts +24 -0
  76. package/dist/lib/config.js +21 -0
  77. package/dist/lib/contract.d.ts +22 -0
  78. package/dist/lib/contract.js +59 -0
  79. package/dist/lib/git.d.ts +6 -0
  80. package/dist/lib/gtm-container.d.ts +41 -0
  81. package/dist/lib/gtm-container.js +145 -0
  82. package/dist/lib/icon-specs.d.ts +6 -0
  83. package/dist/lib/icon-specs.js +6 -0
  84. package/dist/lib/icons.d.ts +6 -0
  85. package/dist/lib/icons.js +32 -0
  86. package/dist/lib/language-words.d.ts +2 -0
  87. package/dist/lib/lighthouse.d.ts +17 -0
  88. package/dist/lib/lighthouse.js +23 -0
  89. package/dist/lib/past-tense.d.ts +1 -0
  90. package/dist/lib/routes.d.ts +31 -0
  91. package/dist/lib/routes.js +97 -0
  92. package/dist/lib/smoke-production.d.ts +35 -0
  93. package/dist/lib/smoke-production.js +132 -0
  94. package/dist/lib/vercel-cli-pin.d.ts +2 -0
  95. package/dist/sh/lhci-local.sh +106 -0
  96. package/package.json +21 -3
@@ -0,0 +1,26 @@
1
+ declare const forSections: (answers: any) => boolean;
2
+ export declare function sectionedCollectionPrompts(plop: any): ({
3
+ type: string;
4
+ name: string;
5
+ message: string;
6
+ when: typeof forSections;
7
+ validate: (value: any) => string | true;
8
+ default?: never;
9
+ } | {
10
+ validate?: never;
11
+ type: string;
12
+ name: string;
13
+ message: string;
14
+ default: boolean;
15
+ when: typeof forSections;
16
+ })[];
17
+ export declare function sectionedCollectionActions(plop: any, answers: any, root: any): ((() => string) | {
18
+ type: string;
19
+ path: string | undefined;
20
+ templateFile: string;
21
+ data: {
22
+ collection: any;
23
+ pageKey: any;
24
+ };
25
+ })[];
26
+ export {};
@@ -0,0 +1,81 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { assertInjectable, injectCollection } from './inject-config.mjs';
4
+ import { assertStringsInjectable, injectStrings } from './inject-strings.mjs';
5
+ import { pageStrings, pathSegments } from './page.mjs';
6
+ import { postGenAction } from './post-gen.mjs';
7
+ import { casings, findSectionedPages, sectionFiles, sectionTargets, validateSectionName } from './section-targets.mjs';
8
+ const SECTIONS = 'templates/sections';
9
+ const SECTION = 'templates/section';
10
+ const ROLLBACK_HINT = 'gen:collection also MODIFIED src/content.config.ts and every dictionary in src/i18n/strings/. Review them ' +
11
+ 'with `git diff`, then discard ONLY the injected hunks with `git checkout -p` on those paths, and delete the ' +
12
+ 'generated barrel, data layer, page and section files. A re-run without rollback fails pre-flight with ' +
13
+ '"already exists".';
14
+ const forSections = (answers) => answers.sections === true;
15
+ export function sectionedCollectionPrompts(plop) {
16
+ return [
17
+ {
18
+ type: 'input',
19
+ name: 'page',
20
+ message: 'Page path of the sectioned page (e.g. about-us):',
21
+ when: forSections,
22
+ validate: (value) => pathSegments(plop, value).length > 0 || 'Page path must contain at least one letter or digit',
23
+ },
24
+ {
25
+ type: 'input',
26
+ name: 'section',
27
+ message: 'First section name (e.g. intro):',
28
+ when: forSections,
29
+ validate: validateSectionName(plop),
30
+ },
31
+ {
32
+ type: 'confirm',
33
+ name: 'image',
34
+ message: 'Does the first section carry an image?',
35
+ default: false,
36
+ when: forSections,
37
+ },
38
+ ];
39
+ }
40
+ function assertCreatable({ root, targets, page, section, strings }) {
41
+ for (const path of [page, targets.barrel, targets.dataLayer, ...sectionFiles(targets, section.kebab)]) {
42
+ if (existsSync(join(root, path))) {
43
+ throw new Error(`gen:collection pre-flight failed: ${path} already exists — remove it or pick another name`);
44
+ }
45
+ }
46
+ const carriers = findSectionedPages(root, targets.sectionsMarker);
47
+ if (carriers.length > 0) {
48
+ throw new Error(`gen:collection pre-flight failed: ${carriers.join(', ')} already carries ${targets.sectionsMarker}`);
49
+ }
50
+ assertStringsInjectable({ root, keys: strings.map(({ key }) => key) });
51
+ }
52
+ export function sectionedCollectionActions(plop, answers, root) {
53
+ const collection = casings(plop, answers.name);
54
+ const section = casings(plop, answers.section);
55
+ const targets = sectionTargets(collection);
56
+ const segments = pathSegments(plop, answers.page);
57
+ const pageKey = plop.getHelper('camelCase')(segments.join('/'));
58
+ const page = `src/pages/${segments.join('/')}.astro`;
59
+ const strings = pageStrings({ pageKey, pageTitle: plop.getHelper('sentenceCase')(segments.at(-1)), dynamic: false });
60
+ const data = { collection: answers.name, pageKey };
61
+ const [schema, content, component] = sectionFiles(targets, section.kebab);
62
+ return [
63
+ () => {
64
+ assertInjectable({ root, camel: collection.camel, kebab: collection.kebab });
65
+ assertCreatable({ root, targets, page, section, strings });
66
+ return 'content.config.ts, page and dictionary contract checks passed';
67
+ },
68
+ { type: 'add', path: targets.barrel, templateFile: `${SECTIONS}/index.ts.hbs`, data },
69
+ { type: 'add', path: targets.dataLayer, templateFile: `${SECTIONS}/data-layer.ts.hbs`, data },
70
+ { type: 'add', path: page, templateFile: `${SECTIONS}/page.astro.hbs`, data },
71
+ { type: 'add', path: schema, templateFile: `${SECTION}/schema.ts.hbs`, data },
72
+ { type: 'add', path: content, templateFile: `${SECTION}/content.yml.hbs`, data },
73
+ { type: 'add', path: component, templateFile: `${SECTION}/component.astro.hbs`, data },
74
+ () => {
75
+ injectCollection({ root, camel: collection.camel, kebab: collection.kebab, sections: true });
76
+ injectStrings({ root, entries: strings });
77
+ return `injected sectioned collection ${collection.kebab} and ${strings.map(({ key }) => key).join(', ')}`;
78
+ },
79
+ postGenAction(root, ROLLBACK_HINT),
80
+ ];
81
+ }
@@ -0,0 +1,2 @@
1
+ title: Esempio
2
+ description: Voce di esempio della collection {{dashCase name}}. Sostituiscila col contenuto reale.
@@ -0,0 +1,8 @@
1
+ ---
2
+ title: Esempio
3
+ description: Voce di esempio della collection {{dashCase name}}.
4
+ pubDate: 2026-01-01
5
+ draft: true
6
+ ---
7
+
8
+ Corpo di esempio della collection **{{dashCase name}}**. Sostituiscilo col contenuto reale.
@@ -0,0 +1,11 @@
1
+ import { z } from 'astro/zod'
2
+
3
+ export const {{camelCase name}}Schema = z.strictObject({
4
+ title: z.string().min(1),
5
+ description: z.string().optional(),
6
+ {{#if document}}
7
+ pubDate: z.coerce.date(),
8
+ draft: z.boolean().default(false),
9
+ {{/if}}
10
+ })
11
+ export type {{pascalCase name}}Entry = z.infer<typeof {{camelCase name}}Schema>
@@ -0,0 +1,21 @@
1
+ ---
2
+ import type { HTMLAttributes } from 'astro/types'
3
+ import { cva, type VariantProps } from 'class-variance-authority'
4
+
5
+ import { cn } from '@/lib/utils'
6
+
7
+ export const {{camelCase name}}Variants = cva('', {
8
+ variants: {
9
+ variant: {
10
+ default: '',
11
+ },
12
+ },
13
+ defaultVariants: { variant: 'default' },
14
+ })
15
+
16
+ interface Props extends HTMLAttributes<'div'>, VariantProps<typeof {{camelCase name}}Variants> {}
17
+
18
+ const { variant, class: className, ...rest } = Astro.props
19
+ ---
20
+
21
+ <div class={cn({{camelCase name}}Variants({ variant }), className)} {...rest}><slot /></div>
@@ -0,0 +1,24 @@
1
+ ---
2
+ import Container from '@/components/ui/container.astro'
3
+ import Heading from '@/components/ui/heading.astro'
4
+ import Section from '@/components/ui/section.astro'
5
+ import MainLayout from '@/layouts/main.astro'
6
+
7
+ import { useTranslations } from '@/i18n/translate'
8
+
9
+ export function getStaticPaths() {
10
+ // TODO: ricava i percorsi da una collection, come nel blueprint content-section del plugin metodo.
11
+ return [{ params: { slug: 'example' } }]
12
+ }
13
+
14
+ const t = useTranslations(Astro.currentLocale)
15
+ const { slug } = Astro.params
16
+ ---
17
+
18
+ <MainLayout title={slug ?? t('page.{{pageKey}}.title')}>
19
+ <Section>
20
+ <Container>
21
+ <Heading as="h1" size="h1">{slug}</Heading>
22
+ </Container>
23
+ </Section>
24
+ </MainLayout>
@@ -0,0 +1,20 @@
1
+ ---
2
+ import Container from '@/components/ui/container.astro'
3
+ import Heading from '@/components/ui/heading.astro'
4
+ import Section from '@/components/ui/section.astro'
5
+ import MainLayout from '@/layouts/main.astro'
6
+
7
+ import { useTranslations } from '@/i18n/translate'
8
+
9
+ const t = useTranslations(Astro.currentLocale)
10
+ const title = t('page.{{pageKey}}.title')
11
+ const description = t('page.{{pageKey}}.description')
12
+ ---
13
+
14
+ <MainLayout {title} {description}>
15
+ <Section>
16
+ <Container>
17
+ <Heading as="h1" size="h1">{title}</Heading>
18
+ </Container>
19
+ </Section>
20
+ </MainLayout>
@@ -0,0 +1,45 @@
1
+ ---
2
+ {{#if image}}
3
+ import { Image } from 'astro:assets'
4
+
5
+ {{/if}}
6
+ import Button from '@/components/ui/button.astro'
7
+ import Container from '@/components/ui/container.astro'
8
+ import Heading from '@/components/ui/heading.astro'
9
+ import Section from '@/components/ui/section.astro'
10
+ import type { {{pascalCase collection}}Sections } from '@/lib/{{dashCase collection}}'
11
+
12
+ import { ctaHref } from '@/i18n/href'
13
+
14
+ type Props = {{pascalCase collection}}Sections['{{camelCase section}}']
15
+
16
+ const { title, body, {{#if image}}image, {{/if}}buttons } = Astro.props
17
+ const href = (url: string) => ctaHref(Astro.currentLocale, url)
18
+ ---
19
+
20
+ <Section class="border-b border-border bg-background">
21
+ <Container>
22
+ <Heading as="h2">{title}</Heading>
23
+ {body && <p class="mt-4 max-w-2xl text-pretty text-muted-foreground">{body}</p>}
24
+ {{#if image}}
25
+ <Image
26
+ src={image.src}
27
+ alt={image.alt}
28
+ widths={[640, 960, 1280, 1920]}
29
+ sizes="(min-width: 96rem) 96rem, 100vw"
30
+ class="mt-8 h-auto w-full rounded-lg"
31
+ />
32
+ {{/if}}
33
+ {
34
+ buttons.length > 0 && (
35
+ <div class="mt-8 flex flex-wrap gap-4">
36
+ {buttons.map((button, index) => (
37
+ <Button as="a" href={href(button.url)} variant={index === 0 ? 'default' : 'outline'}>
38
+ {button.label}
39
+ </Button>
40
+ ))}
41
+ </div>
42
+ )
43
+ }
44
+ </Container>
45
+ </Section>
@@ -0,0 +1,10 @@
1
+ section: {{camelCase section}}
2
+ title: Titolo della sezione
3
+ body: >-
4
+ Contenuto placeholder della sezione. Sostituiscilo col copy reale del progetto.
5
+ {{#if image}}
6
+ image:
7
+ src: ../../assets/placeholder.jpg
8
+ alt: Descrizione dell'immagine
9
+ {{/if}}
10
+ buttons: []
@@ -0,0 +1,20 @@
1
+ {{#if image}}
2
+ import type { SchemaContext } from 'astro:content'
3
+ {{/if}}
4
+ import { z } from 'astro/zod'
5
+
6
+ import { ctaSchema{{#if image}}, imageSchema{{/if}} } from '../common'
7
+
8
+ export function {{camelCase section}}SectionSchema({{#if image}}{ image }: SchemaContext{{/if}}) {
9
+ return z.strictObject({
10
+ section: z.literal('{{camelCase section}}'),
11
+ title: z.string().min(1),
12
+ body: z.string().optional(),
13
+ {{#if image}}
14
+ image: imageSchema(image),
15
+ {{/if}}
16
+ buttons: z.array(ctaSchema).default([]),
17
+ })
18
+ }
19
+
20
+ export type {{pascalCase section}}Section = z.infer<ReturnType<typeof {{camelCase section}}SectionSchema>>
@@ -0,0 +1,13 @@
1
+ import { loadLocalizedSections, type SectionedPage } from '@/lib/content/localized-sections'
2
+
3
+ export type {{pascalCase collection}}Sections = SectionedPage<'{{dashCase collection}}'>
4
+
5
+ export async function get{{pascalCase collection}}Sections(locale?: string): Promise<{{pascalCase collection}}Sections> {
6
+ const { pick } = await loadLocalizedSections('{{dashCase collection}}', locale)
7
+
8
+ // INJECTION POINT per `pnpm gen:section` (ts-morph): le sezioni nuove aggiungono qui il
9
+ // loro pick() — il generatore verifica questo letterale di ritorno.
10
+ return {
11
+ {{camelCase section}}: pick('{{camelCase section}}'),
12
+ }
13
+ }
@@ -0,0 +1,12 @@
1
+ {{#if image}}
2
+ import type { SchemaContext } from 'astro:content'
3
+ {{/if}}
4
+ import { z } from 'astro/zod'
5
+
6
+ import { {{camelCase section}}SectionSchema } from './{{dashCase section}}'
7
+
8
+ // INJECTION POINT per `pnpm gen:section` (ts-morph): il generatore verifica questa funzione
9
+ // e la sua chiamata a z.discriminatedUnion.
10
+ export function {{camelCase collection}}CollectionSchema({{#if image}}context: SchemaContext{{/if}}) {
11
+ return z.discriminatedUnion('section', [{{camelCase section}}SectionSchema({{#if image}}context{{/if}})])
12
+ }
@@ -0,0 +1,30 @@
1
+ ---
2
+ import {{pascalCase section}} from '@/components/{{dashCase collection}}/{{dashCase section}}.astro'
3
+ import Container from '@/components/ui/container.astro'
4
+ import Heading from '@/components/ui/heading.astro'
5
+ import Section from '@/components/ui/section.astro'
6
+
7
+ import MainLayout from '@/layouts/main.astro'
8
+
9
+ import { get{{pascalCase collection}}Sections } from '@/lib/{{dashCase collection}}'
10
+
11
+ import { useTranslations } from '@/i18n/translate'
12
+
13
+ // @gen:{{dashCase collection}}-imports (i generatori inseriscono i nuovi import SOPRA questa riga)
14
+
15
+ const t = useTranslations(Astro.currentLocale)
16
+ const title = t('page.{{pageKey}}.title')
17
+ const description = t('page.{{pageKey}}.description')
18
+ const content = await get{{pascalCase collection}}Sections(Astro.currentLocale)
19
+ ---
20
+
21
+ <MainLayout {title} {description}>
22
+ <Section>
23
+ <Container>
24
+ <Heading as="h1" size="h1">{title}</Heading>
25
+ </Container>
26
+ </Section>
27
+ {{!-- Handlebars legge "}}}" come chiusura di un'espressione non escapata: il tilde chiude il mustache prima della graffa di Astro. --}}
28
+ <{{pascalCase section}} {...content.{{camelCase section~}}} />
29
+ {/* @gen:{{dashCase collection}}-sections */}
30
+ </MainLayout>
@@ -0,0 +1 @@
1
+ export declare function isNameTaken(sourceFile: any, name: any): any;
@@ -0,0 +1,8 @@
1
+ export function isNameTaken(sourceFile, name) {
2
+ if (sourceFile.getVariableDeclaration(name) || sourceFile.getFunction(name))
3
+ return true;
4
+ return sourceFile
5
+ .getImportDeclarations()
6
+ .some((d) => d.getNamedImports().some((n) => (n.getAliasNode()?.getText() ?? n.getName()) === name) ||
7
+ d.getDefaultImport()?.getText() === name);
8
+ }
@@ -0,0 +1,10 @@
1
+ export type { Budget } from './lib/bundle-budget.ts';
2
+ export { CSS_BUDGET_GZIP } from './lib/bundle-budget.ts';
3
+ export type { OfficinaConfig } from './lib/config.ts';
4
+ export { defineConfig } from './lib/config.ts';
5
+ export type { IconSpec } from './lib/icon-specs.ts';
6
+ export { ICON_SPECS } from './lib/icon-specs.ts';
7
+ export type { VerifiedRoute } from './lib/routes.ts';
8
+ export { NON_HTML_ROUTES } from './lib/routes.ts';
9
+ export type { CheckResult, SmokeCheck, SmokeContext } from './lib/smoke-production.ts';
10
+ export { checkBotIdChallenge, checkCanonicalHost, checkPages, checkSecurityHeaders, checkTrailingSlash, DEFAULT_CHECKS, SECURITY_HEADERS, } from './lib/smoke-production.ts';
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { CSS_BUDGET_GZIP } from './lib/bundle-budget.js';
2
+ export { defineConfig } from './lib/config.js';
3
+ export { ICON_SPECS } from './lib/icon-specs.js';
4
+ export { NON_HTML_ROUTES } from './lib/routes.js';
5
+ export { checkBotIdChallenge, checkCanonicalHost, checkPages, checkSecurityHeaders, checkTrailingSlash, DEFAULT_CHECKS, SECURITY_HEADERS, } from './lib/smoke-production.js';
@@ -0,0 +1,12 @@
1
+ import type { Trigger } from './gtm-container.ts';
2
+ export type LinkEvent = {
3
+ prefix: string;
4
+ event: string;
5
+ };
6
+ export type Coverage = {
7
+ event: string;
8
+ prefix: string;
9
+ covered: boolean;
10
+ };
11
+ export declare function extractLinkEvents(source: string): LinkEvent[];
12
+ export declare function coverageOf(events: readonly LinkEvent[], triggers: readonly Trigger[]): Coverage[];
@@ -0,0 +1,15 @@
1
+ const LINK_EVENT = /startsWith\('([^']+)'\)\)\s*return\s*'([^']+)'/g;
2
+ // Letto dal sorgente di src/lib/analytics/link-tracking.ts invece di essere riscritto qui: le
3
+ // due liste divergono il giorno in cui qualcuno aggiunge uno schema, e non fallirebbe niente.
4
+ export function extractLinkEvents(source) {
5
+ return [...source.matchAll(LINK_EVENT)].map(([, prefix, event]) => ({
6
+ /* v8 ignore start -- i due gruppi di LINK_EVENT sono obbligatori: il ripiego serve solo ai tipi */
7
+ prefix: prefix ?? '',
8
+ event: event ?? '',
9
+ /* v8 ignore stop */
10
+ }));
11
+ }
12
+ export function coverageOf(events, triggers) {
13
+ const fired = new Set(triggers.map(({ eventName }) => eventName));
14
+ return events.map(({ event, prefix }) => ({ event, prefix, covered: fired.has(event) }));
15
+ }
@@ -0,0 +1,24 @@
1
+ export type Chunk = {
2
+ gzip: number;
3
+ static: Set<string>;
4
+ dynamic: Set<string>;
5
+ };
6
+ export type Budget = {
7
+ label: string;
8
+ matches: (route: string) => boolean;
9
+ maxGzip: number;
10
+ };
11
+ /** Le classi del progetto vengono prima, nell'ordine in cui `officina.config.ts` le dichiara. */
12
+ export declare function budgetFor(route: string, budgets?: readonly Budget[]): Budget;
13
+ /** Rollup racchiude gli specificatori statici con `"` e i dinamici in un template literal. */
14
+ export declare function parseEdges(source: string): Pick<Chunk, 'static' | 'dynamic'>;
15
+ export declare function htmlEntries(html: string): string[];
16
+ export declare function staticClosure(entries: Iterable<string>, chunks: Map<string, Chunk>): Set<string>;
17
+ export declare function deferredClosure(reached: Set<string>, chunks: Map<string, Chunk>): Set<string>;
18
+ export declare const CSS_BUDGET_GZIP: number;
19
+ export type Stylesheet = {
20
+ file: string;
21
+ gzip: number;
22
+ };
23
+ export declare function heaviestStylesheet(sheets: readonly Stylesheet[]): Stylesheet | null;
24
+ export declare function cssBudgetFailure(sheets: readonly Stylesheet[], maxGzip?: number): string | null;
@@ -0,0 +1,50 @@
1
+ // 20 KB è ~1,5× la rotta più pesante misurata nello starter (/contatti, 13,4 KB gz).
2
+ const DEFAULT_BUDGET = { label: 'default', matches: () => true, maxGzip: 20 * 1024 };
3
+ /** Le classi del progetto vengono prima, nell'ordine in cui `officina.config.ts` le dichiara. */
4
+ export function budgetFor(route, budgets = []) {
5
+ /* v8 ignore next -- DEFAULT_BUDGET accetta ogni rotta, quindi find() non torna mai undefined */
6
+ return [...budgets, DEFAULT_BUDGET].find((budget) => budget.matches(route)) ?? DEFAULT_BUDGET;
7
+ }
8
+ const captured = (source, pattern, group) => new Set([...source.matchAll(pattern)].map((match) => match[group]).filter((name) => name !== undefined));
9
+ /** Rollup racchiude gli specificatori statici con `"` e i dinamici in un template literal. */
10
+ export function parseEdges(source) {
11
+ return {
12
+ static: captured(source, /(?:from|import)\s*(["'`])\.\/([^"'`]+\.js)\1/g, 2),
13
+ dynamic: captured(source, /import\(\s*(["'`])\.\/([^"'`]+\.js)\1\s*\)/g, 2),
14
+ };
15
+ }
16
+ export function htmlEntries(html) {
17
+ return [...captured(html, /(?:src|href)="\/_astro\/([^"]+\.js)"/g, 1)];
18
+ }
19
+ export function staticClosure(entries, chunks) {
20
+ const seen = new Set();
21
+ const queue = [...entries];
22
+ while (queue.length > 0) {
23
+ const name = queue.pop();
24
+ if (name === undefined || seen.has(name))
25
+ continue;
26
+ const chunk = chunks.get(name);
27
+ if (chunk === undefined)
28
+ continue;
29
+ seen.add(name);
30
+ queue.push(...chunk.static);
31
+ }
32
+ return seen;
33
+ }
34
+ export function deferredClosure(reached, chunks) {
35
+ const entries = [...reached].flatMap((name) => [...(chunks.get(name)?.dynamic ?? [])]);
36
+ return new Set([...staticClosure(entries, chunks)].filter((name) => !reached.has(name)));
37
+ }
38
+ export const CSS_BUDGET_GZIP = 12 * 1024;
39
+ export function heaviestStylesheet(sheets) {
40
+ return sheets.reduce((worst, sheet) => (worst && worst.gzip >= sheet.gzip ? worst : sheet), null);
41
+ }
42
+ // Astro emette un foglio di stile per gruppo di pagine e una rotta ne collega esattamente uno,
43
+ // quindi la loro somma è fatta di byte che nessun visitatore scarica mai insieme.
44
+ export function cssBudgetFailure(sheets, maxGzip = CSS_BUDGET_GZIP) {
45
+ const heaviest = heaviestStylesheet(sheets);
46
+ if (heaviest === null || heaviest.gzip <= maxGzip)
47
+ return null;
48
+ const over = heaviest.gzip - maxGzip;
49
+ return `CSS ${(heaviest.gzip / 1024).toFixed(1)} KB gz > ${(maxGzip / 1024).toFixed(1)} KB (+${(over / 1024).toFixed(1)} KB) in ${heaviest.file}`;
50
+ }
@@ -0,0 +1,10 @@
1
+ import type { Hit } from './cli.ts';
2
+ import { type Line } from './comment-syntax.ts';
3
+ export interface FileReport {
4
+ findings: Hit[];
5
+ comments: number;
6
+ total: number;
7
+ }
8
+ export declare const SCANNED: RegExp;
9
+ export declare function report(file: string, lines: Line[]): FileReport;
10
+ export declare function isNoisy({ comments, total }: FileReport): boolean;
@@ -0,0 +1,11 @@
1
+ import type { Hit } from './cli.ts';
2
+ export declare function isScanned(path: string): boolean;
3
+ export interface Tally {
4
+ italian: number;
5
+ english: number;
6
+ }
7
+ export declare function tally(prose: string): Tally;
8
+ export type Verdict = 'english' | 'italian' | 'undecided';
9
+ export declare function classify({ italian, english }: Tally, minHits?: number): Verdict;
10
+ /** In un sorgente la prosa sta solo nei commenti: il codice è inglese per costruzione. */
11
+ export declare function findingsFor(path: string, source: string): Hit[];
@@ -0,0 +1,6 @@
1
+ import { type Finding, type Hit } from './cli.ts';
2
+ type EnvHit = Omit<Finding, 'path' | 'severity'>;
3
+ export declare function placeholderSources(): string[];
4
+ export declare function sourceFindings(source: string): Hit[];
5
+ export declare function envFindings(content: string): EnvHit[];
6
+ export {};
@@ -0,0 +1,30 @@
1
+ import type { Hit } from './cli.ts';
2
+ export interface Row {
3
+ line: number;
4
+ number: number;
5
+ name: string;
6
+ phase: number;
7
+ days: number;
8
+ }
9
+ export interface Subtotal {
10
+ line: number;
11
+ label: string;
12
+ phase: number | null;
13
+ days: number;
14
+ }
15
+ export interface Section {
16
+ line: number;
17
+ number: number;
18
+ name: string;
19
+ phase: number;
20
+ days: number;
21
+ }
22
+ /** «1,5» è una giornata e mezza. Una cella vuota non è zero giornate: `Number('')` sarebbe 0. */
23
+ export declare function parseDays(raw: string): number;
24
+ /** Le righe numerate della tabella Status: una per milestone. */
25
+ export declare function statusRows(source: string): Row[];
26
+ /** Le righe di somma: un subtotale per fase, più il totale. */
27
+ export declare function subtotals(source: string): Subtotal[];
28
+ /** Le sezioni `## Milestone N`, con la fase e le giornate della loro testata. */
29
+ export declare function sections(source: string): Section[];
30
+ export declare function findingsFor(source: string): Hit[];
@@ -0,0 +1,6 @@
1
+ import type { Hit } from './cli.ts';
2
+ export declare const isScanned: (path: string) => boolean;
3
+ export declare function normalizeTitle(raw: string): string;
4
+ export declare function headings(source: string): string[];
5
+ export declare function resolveTarget(from: string, cited: string): string;
6
+ export declare function findingsFor(path: string, source: string): Hit[];
@@ -0,0 +1,26 @@
1
+ export interface Hit {
2
+ line: number;
3
+ message: string;
4
+ }
5
+ export type Finding = {
6
+ path: string;
7
+ line?: number;
8
+ message: string;
9
+ severity: 'error' | 'warning';
10
+ };
11
+ export declare function cliOptions(args?: string[]): {
12
+ base?: string;
13
+ head?: string;
14
+ strict: boolean;
15
+ format: string;
16
+ diff: boolean;
17
+ };
18
+ export type CliOptions = ReturnType<typeof cliOptions>;
19
+ export declare const isGenerated: (path: string) => boolean;
20
+ export declare function scopedPaths(options: CliOptions, isScanned: (path: string) => boolean): string[];
21
+ export declare function printFindings(findings: Finding[], format: string): void;
22
+ export declare function exitCode(findings: Finding[], strict: boolean): number;
23
+ export declare const readLines: (text: string) => {
24
+ n: number;
25
+ text: string;
26
+ }[];
@@ -0,0 +1,11 @@
1
+ export interface Line {
2
+ n: number;
3
+ text: string;
4
+ }
5
+ export interface CommentBlock {
6
+ lines: Line[];
7
+ trailing: boolean;
8
+ }
9
+ export type CommentStyle = 'hash' | 'css' | 'slash' | 'astro';
10
+ export declare function styleOf(file: string): CommentStyle;
11
+ export declare function commentBlocks(lines: Line[], style: CommentStyle): CommentBlock[];
@@ -0,0 +1,24 @@
1
+ import type { Budget } from './bundle-budget.ts';
2
+ import type { VerifiedRoute } from './routes.ts';
3
+ import type { SmokeCheck } from './smoke-production.ts';
4
+ export interface OfficinaConfig {
5
+ /** L'apice di produzione, senza barra finale: lo usa `check smoke` quando non riceve un URL. */
6
+ siteUrl?: string;
7
+ icons?: {
8
+ background: string;
9
+ };
10
+ bundle?: {
11
+ budgets?: readonly Budget[];
12
+ cssMaxGzip?: number;
13
+ };
14
+ smoke?: {
15
+ securityHeaders?: Record<string, string | null>;
16
+ nonHtmlRoutes?: readonly VerifiedRoute[];
17
+ checks?: readonly SmokeCheck[];
18
+ };
19
+ analytics?: {
20
+ linkTracking?: string;
21
+ };
22
+ }
23
+ export declare const defineConfig: (config: OfficinaConfig) => OfficinaConfig;
24
+ export declare function loadConfig(root: string): Promise<OfficinaConfig>;
@@ -0,0 +1,21 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ import { existsSync } from 'node:fs';
10
+ import { join } from 'node:path';
11
+ import { pathToFileURL } from 'node:url';
12
+ const CONFIG_FILES = ['officina.config.ts', 'officina.config.mjs', 'officina.config.js'];
13
+ export const defineConfig = (config) => config;
14
+ // Fuori da node_modules Node toglie i tipi da solo: il file del progetto può essere TypeScript.
15
+ export async function loadConfig(root) {
16
+ const file = CONFIG_FILES.map((name) => join(root, name)).find((path) => existsSync(path));
17
+ if (file === undefined)
18
+ return {};
19
+ const module = await import(__rewriteRelativeImportExtension(pathToFileURL(file).href));
20
+ return module.default ?? {};
21
+ }