@easy-web/swa 1.0.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/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # @easy-web/swa
2
+
3
+ Astro integration that manages Azure Static Web Apps (SWA) 404 handling via a sentinel-marked sidecar file. Ensures the integration's configuration changes are preserved across rebuilds while respecting user-authored settings.
4
+
5
+ ## What it does
6
+
7
+ When you add `easyWebNotFound()` to your Astro config, the integration:
8
+
9
+ 1. **Reads** the existing `staticwebapp.config.json` at build time (if present)
10
+ 2. **Emits** a global 404 response override (`responseOverrides.404`) that rewrites unmatched routes to `/404.html`
11
+ 3. **Tracks ownership** via a sidecar file (`staticwebapp.config.json.easy-web-managed.json`) so future builds know which settings the integration manages
12
+ 4. **Preserves** all user-authored settings: `auth`, `globalHeaders`, `navigationFallback`, custom routes, and any other `responseOverrides` the user defined
13
+
14
+ ## Usage
15
+
16
+ ```ts
17
+ // astro.config.mjs
18
+ import { defineConfig } from 'astro/config';
19
+ import easyWebNotFound from '@easy-web/swa';
20
+
21
+ export default defineConfig({
22
+ integrations: [
23
+ easyWebNotFound(),
24
+ ],
25
+ });
26
+ ```
27
+
28
+ The integration requires no configuration. The `Options` type (with optional `defaultLocale` and `locales` fields) is retained for API compatibility but has no effect in v0.2.0+.
29
+
30
+ ## How it works
31
+
32
+ ### The sidecar file
33
+
34
+ SWA's `staticwebapp.config.json` schema uses `additionalProperties: false` at the root, which means the config file cannot store metadata about which keys are managed by which tool. To solve this, the integration maintains a sibling file:
35
+
36
+ ```json
37
+ // staticwebapp.config.json.easy-web-managed.json
38
+ {
39
+ "keys": ["responseOverrides.404"],
40
+ "version": "0.2.0",
41
+ "docs": "https://github.com/achimismaili/websites/blob/main/docs/decisions/0013-shared-not-found-primitives.md"
42
+ }
43
+ ```
44
+
45
+ On the next build, the integration reads this sidecar to determine which keys it previously claimed. If the sidecar says it owns `responseOverrides.404`, the integration updates it; otherwise, it leaves the user's 404 override untouched.
46
+
47
+ ### Single global 404 limitation
48
+
49
+ Azure Static Web Apps supports only **one global 404 response override**. This integration emits a single, locale-agnostic 404 handler. If your site uses i18n:
50
+
51
+ - Unmatched routes in any locale will serve the **default-locale 404 body** (from `/404.html`)
52
+ - Per-locale 404 content is **not supported** by this integration
53
+ - If you need locale-specific 404 pages, you must implement them outside this integration (e.g., via Astro routing or a custom SWA configuration)
54
+
55
+ See [ADR 0013 — Shared Not-Found Primitives](https://github.com/achimismaili/websites/blob/main/docs/decisions/0013-shared-not-found-primitives.md) for the full rationale.
56
+
57
+ ## Preservation guarantees
58
+
59
+ The integration **only manages** `responseOverrides.404`. All other settings are preserved exactly as you authored them:
60
+
61
+ - ✅ `auth` — untouched
62
+ - ✅ `globalHeaders` — untouched
63
+ - ✅ `navigationFallback` — untouched
64
+ - ✅ `routes` — untouched
65
+ - ✅ Other `responseOverrides` — untouched
66
+ - ✅ All other root keys — untouched
67
+
68
+ If you define your own `responseOverrides.404` before the integration runs, the integration will detect this and skip managing the 404 override, logging a warning instead.
69
+
70
+ ## Compatibility
71
+
72
+ - **Astro:** `>=6.0.0 <8.0.0`
73
+ - **Node.js:** `>=18.0.0`
@@ -0,0 +1,22 @@
1
+ import type { AstroIntegration } from 'astro';
2
+ /**
3
+ * Options retained for compatibility with the 0.1.x API.
4
+ *
5
+ * Azure Static Web Apps supports one global 404 response override, so locale
6
+ * settings no longer affect the emitted configuration.
7
+ */
8
+ export type Options = {
9
+ readonly defaultLocale?: string;
10
+ readonly locales?: readonly string[];
11
+ };
12
+ /**
13
+ * Astro integration that adds shared Azure Static Web Apps 404 handling.
14
+ *
15
+ * The main config contains only schema-legal keys. Ownership metadata lives in
16
+ * a sibling sidecar, and the integration updates `responseOverrides.404` only
17
+ * when that sidecar says the key is managed. User routes and every other SWA
18
+ * setting are preserved; no locale-wide rewrite routes are emitted.
19
+ */
20
+ export declare function easyWebNotFound(_options?: Options): AstroIntegration;
21
+ export default easyWebNotFound;
22
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,OAAO,CAAC;AAK9C;;;;;GAKG;AACH,MAAM,MAAM,OAAO,GAAG;IACpB,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACtC,CAAC;AAkCF;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,QAAQ,GAAE,OAAY,GAAG,gBAAgB,CAkCxE;AAED,eAAe,eAAe,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,152 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ const SENTINEL_VERSION = '0.2.0';
5
+ const SENTINEL_DOCS = 'https://github.com/achimismaili/websites/blob/main/docs/decisions/0013-shared-not-found-primitives.md';
6
+ const KEY_RESPONSE_OVERRIDES_404 = 'responseOverrides.404';
7
+ const SIDECAR_SUFFIX = '.easy-web-managed.json';
8
+ const SCHEMA_LEGAL_ROOT_KEYS = new Set([
9
+ '$schema',
10
+ 'routes',
11
+ 'navigationFallback',
12
+ 'responseOverrides',
13
+ 'mimeTypes',
14
+ 'globalHeaders',
15
+ 'auth',
16
+ 'networking',
17
+ 'forwardingGateway',
18
+ 'platform',
19
+ 'trailingSlash',
20
+ ]);
21
+ const MANAGED_RESPONSE_OVERRIDE = {
22
+ rewrite: '/404.html',
23
+ statusCode: 404,
24
+ };
25
+ /**
26
+ * Astro integration that adds shared Azure Static Web Apps 404 handling.
27
+ *
28
+ * The main config contains only schema-legal keys. Ownership metadata lives in
29
+ * a sibling sidecar, and the integration updates `responseOverrides.404` only
30
+ * when that sidecar says the key is managed. User routes and every other SWA
31
+ * setting are preserved; no locale-wide rewrite routes are emitted.
32
+ */
33
+ export function easyWebNotFound(_options = {}) {
34
+ return {
35
+ name: '@easy-web/swa',
36
+ hooks: {
37
+ 'astro:config:setup': ({ config }) => {
38
+ if (!config.i18n) {
39
+ console.info('[easy-web-swa] no i18n config found — single-locale mode, emitting the global 404 override');
40
+ }
41
+ if (config.output === 'server') {
42
+ console.warn('[easy-web-swa] non-static output detected; integration will write staticwebapp.config.json directly to dist/ instead of relying on public/ passthrough');
43
+ }
44
+ },
45
+ 'astro:build:done': ({ dir }) => {
46
+ const distDir = fileURLToPath(dir);
47
+ const configPath = path.join(distDir, 'staticwebapp.config.json');
48
+ const sidecarPath = `${configPath}${SIDECAR_SUFFIX}`;
49
+ const existing = readConfig(configPath);
50
+ const previousSentinel = readSidecar(sidecarPath);
51
+ const { config, sentinel } = mergeConfig(existing, previousSentinel, configPath);
52
+ writeJson(configPath, config);
53
+ writeJson(sidecarPath, sentinel);
54
+ },
55
+ },
56
+ };
57
+ }
58
+ export default easyWebNotFound;
59
+ function readConfig(configPath) {
60
+ if (!fs.existsSync(configPath))
61
+ return null;
62
+ return parseObjectFile(configPath);
63
+ }
64
+ function readSidecar(sidecarPath) {
65
+ if (!fs.existsSync(sidecarPath))
66
+ return null;
67
+ const value = parseObjectFile(sidecarPath);
68
+ const keys = value['keys'];
69
+ const version = value['version'];
70
+ const docs = value['docs'];
71
+ if (!Array.isArray(keys) ||
72
+ !keys.every((key) => typeof key === 'string') ||
73
+ typeof version !== 'string' ||
74
+ typeof docs !== 'string') {
75
+ throw new TypeError(`[easy-web-swa] invalid managed metadata in ${sidecarPath}`);
76
+ }
77
+ return { keys, version, docs };
78
+ }
79
+ function parseObjectFile(filePath) {
80
+ const raw = fs.readFileSync(filePath, 'utf-8');
81
+ let value;
82
+ try {
83
+ value = JSON.parse(raw);
84
+ }
85
+ catch (error) {
86
+ if (error instanceof SyntaxError) {
87
+ throw new SyntaxError(`[easy-web-swa] failed to parse JSON in ${filePath}: ${error.message}`);
88
+ }
89
+ throw error;
90
+ }
91
+ if (!isJsonObject(value)) {
92
+ throw new TypeError(`[easy-web-swa] expected a JSON object in ${filePath}`);
93
+ }
94
+ return value;
95
+ }
96
+ function mergeConfig(existing, previousSentinel, configPath) {
97
+ const config = schemaLegalConfig(existing);
98
+ const responseOverrides = readResponseOverrides(config, configPath);
99
+ const integrationOwns404 = previousSentinel?.keys.includes(KEY_RESPONSE_OVERRIDES_404) ?? false;
100
+ const userOwns404 = '404' in responseOverrides && !integrationOwns404;
101
+ if (userOwns404) {
102
+ console.warn('[easy-web-swa] user has defined responseOverrides.404; user override wins; integration will not manage it');
103
+ return { config, sentinel: makeSentinel([]) };
104
+ }
105
+ return {
106
+ config: {
107
+ ...config,
108
+ responseOverrides: {
109
+ ...responseOverrides,
110
+ '404': MANAGED_RESPONSE_OVERRIDE,
111
+ },
112
+ },
113
+ sentinel: makeSentinel([KEY_RESPONSE_OVERRIDES_404]),
114
+ };
115
+ }
116
+ function schemaLegalConfig(existing) {
117
+ if (existing === null)
118
+ return {};
119
+ const config = {};
120
+ for (const [key, value] of Object.entries(existing)) {
121
+ if (SCHEMA_LEGAL_ROOT_KEYS.has(key)) {
122
+ config[key] = value;
123
+ }
124
+ else {
125
+ console.warn(`[easy-web-swa] omitting non-schema-legal root key "${key}" from staticwebapp.config.json`);
126
+ }
127
+ }
128
+ return config;
129
+ }
130
+ function readResponseOverrides(config, configPath) {
131
+ const value = config['responseOverrides'];
132
+ if (value === undefined)
133
+ return {};
134
+ if (!isJsonObject(value)) {
135
+ throw new TypeError(`[easy-web-swa] expected responseOverrides to be an object in ${configPath}`);
136
+ }
137
+ return value;
138
+ }
139
+ function isJsonObject(value) {
140
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
141
+ }
142
+ function makeSentinel(keys) {
143
+ return {
144
+ keys,
145
+ version: SENTINEL_VERSION,
146
+ docs: SENTINEL_DOCS,
147
+ };
148
+ }
149
+ function writeJson(filePath, value) {
150
+ fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf-8');
151
+ }
152
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAazC,MAAM,gBAAgB,GAAG,OAAO,CAAC;AACjC,MAAM,aAAa,GACjB,uGAAuG,CAAC;AAC1G,MAAM,0BAA0B,GAAG,uBAAuB,CAAC;AAC3D,MAAM,cAAc,GAAG,wBAAwB,CAAC;AAChD,MAAM,sBAAsB,GAAG,IAAI,GAAG,CAAC;IACrC,SAAS;IACT,QAAQ;IACR,oBAAoB;IACpB,mBAAmB;IACnB,WAAW;IACX,eAAe;IACf,MAAM;IACN,YAAY;IACZ,mBAAmB;IACnB,UAAU;IACV,eAAe;CAChB,CAAC,CAAC;AAUH,MAAM,yBAAyB,GAAG;IAChC,OAAO,EAAE,WAAW;IACpB,UAAU,EAAE,GAAG;CACP,CAAC;AAEX;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAC,WAAoB,EAAE;IACpD,OAAO;QACL,IAAI,EAAE,eAAe;QACrB,KAAK,EAAE;YACL,oBAAoB,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE;gBACnC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;oBACjB,OAAO,CAAC,IAAI,CACV,4FAA4F,CAC7F,CAAC;gBACJ,CAAC;gBAED,IAAI,MAAM,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;oBAC/B,OAAO,CAAC,IAAI,CACV,wJAAwJ,CACzJ,CAAC;gBACJ,CAAC;YACH,CAAC;YACD,kBAAkB,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE;gBAC9B,MAAM,OAAO,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC;gBACnC,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,0BAA0B,CAAC,CAAC;gBAClE,MAAM,WAAW,GAAG,GAAG,UAAU,GAAG,cAAc,EAAE,CAAC;gBACrD,MAAM,QAAQ,GAAG,UAAU,CAAC,UAAU,CAAC,CAAC;gBACxC,MAAM,gBAAgB,GAAG,WAAW,CAAC,WAAW,CAAC,CAAC;gBAClD,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,WAAW,CACtC,QAAQ,EACR,gBAAgB,EAChB,UAAU,CACX,CAAC;gBAEF,SAAS,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;gBAC9B,SAAS,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;YACnC,CAAC;SACF;KACF,CAAC;AACJ,CAAC;AAED,eAAe,eAAe,CAAC;AAE/B,SAAS,UAAU,CAAC,UAAkB;IACpC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC;QAAE,OAAO,IAAI,CAAC;IAC5C,OAAO,eAAe,CAAC,UAAU,CAAC,CAAC;AACrC,CAAC;AAED,SAAS,WAAW,CAAC,WAAmB;IACtC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC;QAAE,OAAO,IAAI,CAAC;IAC7C,MAAM,KAAK,GAAG,eAAe,CAAC,WAAW,CAAC,CAAC;IAC3C,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC;IAC3B,MAAM,OAAO,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC;IACjC,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC;IAE3B,IACE,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;QACpB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,OAAO,GAAG,KAAK,QAAQ,CAAC;QAC7C,OAAO,OAAO,KAAK,QAAQ;QAC3B,OAAO,IAAI,KAAK,QAAQ,EACxB,CAAC;QACD,MAAM,IAAI,SAAS,CACjB,8CAA8C,WAAW,EAAE,CAC5D,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AACjC,CAAC;AAED,SAAS,eAAe,CAAC,QAAgB;IACvC,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAC/C,IAAI,KAAc,CAAC;IACnB,IAAI,CAAC;QACH,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC1B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,KAAK,YAAY,WAAW,EAAE,CAAC;YACjC,MAAM,IAAI,WAAW,CACnB,0CAA0C,QAAQ,KAAK,KAAK,CAAC,OAAO,EAAE,CACvE,CAAC;QACJ,CAAC;QACD,MAAM,KAAK,CAAC;IACd,CAAC;IAED,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,SAAS,CAAC,4CAA4C,QAAQ,EAAE,CAAC,CAAC;IAC9E,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,WAAW,CAClB,QAA2B,EAC3B,gBAAwC,EACxC,UAAkB;IAElB,MAAM,MAAM,GAAG,iBAAiB,CAAC,QAAQ,CAAC,CAAC;IAC3C,MAAM,iBAAiB,GAAG,qBAAqB,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACpE,MAAM,kBAAkB,GACtB,gBAAgB,EAAE,IAAI,CAAC,QAAQ,CAAC,0BAA0B,CAAC,IAAI,KAAK,CAAC;IACvE,MAAM,WAAW,GAAG,KAAK,IAAI,iBAAiB,IAAI,CAAC,kBAAkB,CAAC;IAEtE,IAAI,WAAW,EAAE,CAAC;QAChB,OAAO,CAAC,IAAI,CACV,2GAA2G,CAC5G,CAAC;QACF,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY,CAAC,EAAE,CAAC,EAAE,CAAC;IAChD,CAAC;IAED,OAAO;QACL,MAAM,EAAE;YACN,GAAG,MAAM;YACT,iBAAiB,EAAE;gBACjB,GAAG,iBAAiB;gBACpB,KAAK,EAAE,yBAAyB;aACjC;SACF;QACD,QAAQ,EAAE,YAAY,CAAC,CAAC,0BAA0B,CAAC,CAAC;KACrD,CAAC;AACJ,CAAC;AAED,SAAS,iBAAiB,CAAC,QAA2B;IACpD,IAAI,QAAQ,KAAK,IAAI;QAAE,OAAO,EAAE,CAAC;IAEjC,MAAM,MAAM,GAAe,EAAE,CAAC;IAC9B,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;QACpD,IAAI,sBAAsB,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YACpC,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;QACtB,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,IAAI,CACV,sDAAsD,GAAG,iCAAiC,CAC3F,CAAC;QACJ,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,qBAAqB,CAC5B,MAAkB,EAClB,UAAkB;IAElB,MAAM,KAAK,GAAG,MAAM,CAAC,mBAAmB,CAAC,CAAC;IAC1C,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,EAAE,CAAC;IACnC,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,SAAS,CACjB,gEAAgE,UAAU,EAAE,CAC7E,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,YAAY,CAAC,KAAc;IAClC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC;AAED,SAAS,YAAY,CAAC,IAAuB;IAC3C,OAAO;QACL,IAAI;QACJ,OAAO,EAAE,gBAAgB;QACzB,IAAI,EAAE,aAAa;KACpB,CAAC;AACJ,CAAC;AAED,SAAS,SAAS,CAAC,QAAgB,EAAE,KAAc;IACjD,EAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;AAC7E,CAAC"}
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@easy-web/swa",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "packageManager": "pnpm@10.34.4",
6
+ "description": "AstroIntegration that merges a sentinel-marked staticwebapp.config.json slice for shared 404 handling on Azure Static Web Apps.",
7
+ "keywords": [
8
+ "astro-integration",
9
+ "azure-static-web-apps",
10
+ "swa",
11
+ "staticwebapp",
12
+ "404",
13
+ "not-found"
14
+ ],
15
+ "main": "./dist/index.js",
16
+ "types": "./dist/index.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "import": "./dist/index.js"
21
+ }
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "README.md"
26
+ ],
27
+ "sideEffects": false,
28
+ "scripts": {
29
+ "build": "tsc -p tsconfig.build.json",
30
+ "test": "vitest run",
31
+ "typecheck": "tsc --noEmit"
32
+ },
33
+ "peerDependencies": {
34
+ "astro": ">=6.0.0 <8.0.0"
35
+ },
36
+ "devDependencies": {
37
+ "astro": "^6.4.0",
38
+ "@types/node": "^22.0.0",
39
+ "typescript": "^5.5.0",
40
+ "vitest": "^3.2.4"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public",
44
+ "provenance": true
45
+ },
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "https://github.com/achimismaili/easy-web.git",
49
+ "directory": "packages/easy-web-swa"
50
+ },
51
+ "license": "MIT"
52
+ }