Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: auto-create required function files #92

Merged
merged 16 commits into from
Nov 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ jobs:
###### Repository/Build Configurations - These values can be configured to match your app requirements. ######
# For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig
app_location: '/demo' # App source code path
api_location: 'demo/api' # Api source code path - optional
api_location: 'demo/build/server' # Api source code path - optional
output_location: 'build/static' # Built app content directory - optional
###### End of Repository/Build Configurations ######
outputs:
Expand Down
74 changes: 51 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,27 +23,7 @@ export default {
};
```

You will need to create an `api/` folder in your project root containing a [`host.json`](https://docs.microsoft.com/en-us/azure/azure-functions/functions-host-json) and a `package.json` (see samples below). The adapter will output the `render` Azure function for SSR in that folder. The `api` folder needs to be in your repo so that Azure can recognize the API at build time. However, you can add `api/render` to your .gitignore so that the generated function is not in source control.

### Sample `host.json`

```json
{
"version": "2.0",
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle",
"version": "[2.*, 3.0.0)"
}
}
```

### Sample `package.json`

It's okay for this to be empty. Not including it causes the Azure Function build to fail.

```json
{}
```
:warning: **IMPORTANT**: you also need to configure your build so that your SvelteKit site deploys properly. Failing to do so will prevent the project from building and deploying. See the next section for instructions.

## Azure configuration

Expand All @@ -52,9 +32,11 @@ When deploying to Azure, you will need to properly [configure your build](https:
| property | value |
| ----------------- | -------------- |
| `app_location` | `./` |
| `api_location` | `api` |
| `api_location` | `build/server` |
| `output_location` | `build/static` |

If you use a custom API directory (see [below](#apiDir)), your `api_location` will be the same as the value you pass to `apiDir`.

## Running locally with the Azure SWA CLI

You can debug using the [Azure Static Web Apps CLI](https://github.com/Azure/static-web-apps-cli). Note that the CLI is currently in preview and you may encounter issues.
Expand All @@ -68,14 +50,60 @@ To run the CLI, install `@azure/static-web-apps-cli` and the [Azure Functions Co
"configurations": {
"app": {
"outputLocation": "./build/static",
"apiLocation": "./api"
"apiLocation": "./build/server"
}
}
}
```

## Options

### apiDir

The directory where the `sk_render` Azure function for SSR will be placed. Most of the time, you shouldn't need to set this.

By default, the adapter will output the `sk_render` Azure function for SSR in the `build/server` folder. If you want to output it to a different directory instead (e.g. if you have additional Azure functions to deploy), you can set this option.

```js
import azure from 'svelte-adapter-azure-swa';

export default {
kit: {
...
adapter: azure({
apiDir: 'custom/api'
geoffrich marked this conversation as resolved.
Show resolved Hide resolved
})
}
};
```

If you set this option, you will also need to create a `host.json` and `package.json` in your API directory. The adapter normally generates these files by default, but skips them when a custom API directory is provided to prevent overwriting any existing files. You can see the default files the adapter generates in [this directory](https://github.com/geoffrich/svelte-adapter-azure-swa/tree/main/files/api).

For instance, by default the adapter outputs these files...

```
build/
└── server/
├── sk_render/
│ ├── function.json
│ └── index.js
├── host.json
├── local.settings.json
└── package.json
```

... but only outputs these files when a custom API directory is provided:

```
custom/
└── api/
└── sk_render/
├── function.json
└── index.js
```

Also note that the adapter reserves the folder prefix `sk_render` and API route prefix `__render` for Azure functions generated by the adapter. So, if you use a custom API directory, you cannot have any other folder starting with `sk_render` or functions available at the `__render` route, since these will conflict with the adapter's Azure functions.

### customStaticWebAppConfig

An object containing additional Azure SWA [configuration options](https://docs.microsoft.com/en-us/azure/static-web-apps/configuration). This will be merged with the `staticwebapp.config.json` generated by the adapter.
Expand Down
1 change: 0 additions & 1 deletion demo/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,3 @@ node_modules
!.env.example
.vercel
.output
/api/render
2 changes: 2 additions & 0 deletions demo/tests/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ test('submits sverdle guess', async ({ page }) => {
await page.goto('/sverdle');
const input = page.locator('input[name=guess]').first();
await expect(input).not.toBeDisabled();
await input.focus();

await page.keyboard.type('AZURE');
await page.keyboard.press('Enter');

await expect(input).toHaveValue('a');
await expect(input).toBeDisabled();
});
16 changes: 0 additions & 16 deletions files/api/function.json

This file was deleted.

File renamed without changes.
6 changes: 6 additions & 0 deletions files/api/local.settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"IsEncrypted": false,
"Values": {
"FUNCTIONS_WORKER_RUNTIME": "node"
}
}
File renamed without changes.
1 change: 1 addition & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ type Options = {
debug?: boolean;
customStaticWebAppConfig?: CustomStaticWebAppConfig;
esbuildOptions?: Pick<esbuild.BuildOptions, 'external'>;
apiDir?: string;
};

export default function plugin(options?: Options): Adapter;
61 changes: 51 additions & 10 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,25 @@ import esbuild from 'esbuild';

const ssrFunctionRoute = '/api/__render';

const functionJson = `
{
"bindings": [
{
"authLevel": "anonymous",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"route": "__render"
},
{
"type": "http",
"direction": "out",
"name": "res"
}
]
}
`;

/**
* Validate the static web app configuration does not override the minimum config for the adapter to work correctly.
* @param config {import('./types/swa').CustomStaticWebAppConfig}
Expand All @@ -28,30 +47,32 @@ function validateCustomConfig(config) {
export default function ({
debug = false,
customStaticWebAppConfig = {},
esbuildOptions = {}
esbuildOptions = {},
apiDir: customApiDir = undefined
} = {}) {
return {
name: 'adapter-azure-swa',

async adapt(builder) {
if (!existsSync(join('api', 'package.json'))) {
throw new Error(
'You need to create a package.json in your `api` directory. See the adapter README for details.'
// TODO: remove for 1.0
if (!customApiDir && existsSync(join('api', 'render'))) {
builder.log.warn(
`Warning: you have an api/render folder but this adapter now uses the build/server folder for API functions. You may need to update your build configuration. Failing to do so could break your deployed site.
Please see the PR for migration instructions: https://github.com/geoffrich/svelte-adapter-azure-swa/pull/92`
);
}

const swaConfig = generateConfig(customStaticWebAppConfig, builder.config.kit.appDir);

const tmp = builder.getBuildDirectory('azure-tmp');
const publish = 'build';
const staticDir = join(publish, 'static');
const apiDir = join('api', 'render');
const apiDir = customApiDir || join(publish, 'server');
const functionDir = join(apiDir, 'sk_render');
const entry = `${tmp}/entry.js`;
builder.log.minor(`Publishing to "${publish}"`);

builder.rimraf(tmp);
builder.rimraf(publish);
builder.rimraf(apiDir);

const files = fileURLToPath(new URL('./files', import.meta.url));

Expand All @@ -68,7 +89,12 @@ export default function ({
}
});

builder.copy(join(files, 'api'), apiDir);
if (customApiDir) {
checkForMissingFiles();
} else {
// if the user specified a custom API directory, assume they are creating the required function files themselves
builder.copy(join(files, 'api'), apiDir);
}

writeFileSync(
`${tmp}/manifest.js`,
Expand All @@ -80,7 +106,7 @@ export default function ({
/** @type {BuildOptions} */
const default_options = {
entryPoints: [entry],
outfile: join(apiDir, 'index.js'),
outfile: join(functionDir, 'index.js'),
bundle: true,
platform: 'node',
target: 'node16',
Expand All @@ -89,6 +115,7 @@ export default function ({
};

await esbuild.build(default_options);
writeFileSync(join(functionDir, 'function.json'), functionJson);

builder.log.minor('Copying assets...');
builder.writeClient(staticDir);
Expand All @@ -111,7 +138,21 @@ export default function ({
);
}

writeFileSync(`${publish}/staticwebapp.config.json`, JSON.stringify(swaConfig));
writeFileSync(`${publish}/staticwebapp.config.json`, JSON.stringify(swaConfig, null, 2));

/**
* Check for missing files when a custom API directory is provided.
*/
function checkForMissingFiles() {
const requiredFiles = ['host.json', 'package.json'];
for (const file of requiredFiles) {
if (!existsSync(join(customApiDir, file))) {
builder.log.warn(
`Warning: apiDir set but ${file} does not exist. You will need to create this file yourself. See the docs for more information: https://github.com/geoffrich/svelte-adapter-azure-swa#apidir`
);
}
}
}
}
};
}
Expand Down
24 changes: 19 additions & 5 deletions test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,27 @@ describe('adapt', () => {
await adapter.adapt(builder);
expect(builder.writePrerendered).toBeCalled();
expect(builder.writeClient).toBeCalled();
expect(builder.copy).toBeCalledWith(expect.stringContaining('api'), 'build/server');
});

test('throws error when no package.json', async () => {
existsSync.mockImplementationOnce(() => false);
test('writes to custom api directory', async () => {
const adapter = azureAdapter({ apiDir: 'custom/api' });
const builder = getMockBuilder();
await adapter.adapt(builder);
expect(writeFileSync).toBeCalledWith(
'custom/api/sk_render/function.json',
expect.stringContaining('__render')
);
// we don't copy the required function files to a custom API directory
expect(builder.copy).not.toBeCalledWith(expect.stringContaining('api'), 'custom/api');
});

const adapter = azureAdapter();
test('logs warning when custom api directory set and required file does not exist', async () => {
vi.mocked(existsSync).mockImplementationOnce(() => false);
const adapter = azureAdapter({ apiDir: 'custom/api' });
const builder = getMockBuilder();
await expect(adapter.adapt(builder)).rejects.toThrowError('You need to create a package.json');
await adapter.adapt(builder);
expect(builder.log.warn).toBeCalled();
});

test('adds index.html when root not prerendered', async () => {
Expand Down Expand Up @@ -112,7 +125,8 @@ function getMockBuilder() {
}
},
log: {
minor: vi.fn()
minor: vi.fn(),
warn: vi.fn()
},
prerendered: {
paths: ['/']
Expand Down